Adamw has uploaded a new change for review.
https://gerrit.wikimedia.org/r/52167
Change subject: WIP statemachine and transformation chain.
......................................................................
WIP statemachine and transformation chain.
Change-Id: I9ce59073496b6baf22c5f5eb2698753cbbe3780d
---
A SmashPig/Core/IStorable.php
A SmashPig/Core/Membrane.php
A SmashPig/Core/ReferenceTracking.php
A SmashPig/Core/StateMachines/AbstractStateMachine.php
A SmashPig/Core/StateMachines/IStateMachine.php
A SmashPig/Core/StateMachines/NetworkEgress.php
A SmashPig/Core/StateMachines/StateMachineDescription.php
A SmashPig/Core/StateMachines/TransactionLifespan.php
A SmashPig/Core/StateMachines/TransitionNotPossibleException.php
A SmashPig/Core/Transformations/IProteanData.php
A SmashPig/Core/Transformations/ITransformation.php
A SmashPig/Core/Transformations/TransformationChain.php
A SmashPig/Core/Transformations/TransformedData.php
13 files changed, 478 insertions(+), 0 deletions(-)
git pull
ssh://gerrit.wikimedia.org:29418/wikimedia/fundraising/PaymentsListeners
refs/changes/67/52167/1
diff --git a/SmashPig/Core/IStorable.php b/SmashPig/Core/IStorable.php
new file mode 100644
index 0000000..7613d67
--- /dev/null
+++ b/SmashPig/Core/IStorable.php
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * A serializable object which may be "frozen" using move semantics.
+ */
+interface IStorable {
+ /**
+ * Save to long-term storage. Must check for external modification.
+ */
+ function sync();
+
+ /**
+ * Synchronize and destroy atomically. After freeze is run, the object
+ * can no longer be used.
+ */
+ function freeze();
+}
diff --git a/SmashPig/Core/Membrane.php b/SmashPig/Core/Membrane.php
new file mode 100644
index 0000000..d8d17ad
--- /dev/null
+++ b/SmashPig/Core/Membrane.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * The coarsest unit of stateful job execution.
+ * Reference tracking is used to protect data during accidental termination.
+ * Children
+ * Delegates most functions to children, and aggregates the results.
+ */
+class Membrane
+ implements /*IContainer, */IStateMachine, IStorable/*, IRefChecked*/,
IProteanData
+{
+ function __construct() {
+ ReferenceTracking::record( $this );
+ }
+
+ function __destruct() {
+ ReferenceTracking::release( $this );
+ }
+
+ protected function eachChild( $operation, $params = array() ) {
+ $result = array();
+ foreach ( $this->getChildren() as $child ) {
+ $result[] = call_user_func_array( array( $child,
$operation ), $params );
+ }
+ return $result;
+ }
+
+ function freeze() {
+ $this->eachChild( 'freeze' );
+ }
+
+ function signal( $name ) {
+ //XXX need to capture child signals and propagate to siblings
+ $this->eachChild( 'signal', func_get_args() );
+ }
+
+ function getDataVariant( string $variation ) {
+ return array_reduce(
+ $this->eachChild( 'getDataVariant', func_get_args() ),
+ 'array_merge_recursive',
+ array()
+ );
+ }
+}
diff --git a/SmashPig/Core/ReferenceTracking.php
b/SmashPig/Core/ReferenceTracking.php
new file mode 100644
index 0000000..6e33c4d
--- /dev/null
+++ b/SmashPig/Core/ReferenceTracking.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Externally track all IRefChecked objects while they are kept in memory.
+ *
+ * A soft shutdown will freeze to long-term storage, if possible.
+ */
+class ReferenceTracking {
+ protected static $refList = array();
+ protected static $exit_hook_installed = false;
+
+ static function record( IStorable &$object ) {
+ if ( !$exit_hook_installed ) {
+ register_shutdown_function( array( 'ReferenceTracking',
'exit_hook' ) );
+ $exit_hook_installed = true;
+ }
+ $id = self::hash( $object );
+ $refList[$id] = &$object;
+ }
+
+ static function release( $object ) {
+ $id = self::hash( $object );
+ if ( array_key_exists( $id, self::$refList ) ) {
+ unset self::$refList[$id];
+ } else {
+ //FIXME: opaque hash is not friendly. dump when in
debug mode.
+ Logger::error( "Tried to double-release tracked object:
$id" );
+ }
+ }
+
+ static function hash( $object ) {
+ return spl_object_hash( $object );
+ }
+
+ static function exit_hook() {
+ foreach ( self::$refList as $id => $object ) {
+ try {
+ if ( $object instanceof IStorable ) {
+ $object->freeze();
+ } else {
+ syslog( LOG_CRIT, "Bad news: corrupt
tracked object: $id" );
+ }
+ } catch ( Exception $ex ) {
+ syslog( LOG_CRIT, "Bad news: exception in
shutdown handler: " . $ex->getMessage() );
+ }
+ }
+ self::$refList = array();
+ }
+}
diff --git a/SmashPig/Core/StateMachines/AbstractStateMachine.php
b/SmashPig/Core/StateMachines/AbstractStateMachine.php
new file mode 100644
index 0000000..986786d
--- /dev/null
+++ b/SmashPig/Core/StateMachines/AbstractStateMachine.php
@@ -0,0 +1,142 @@
+<?php namespace SmashPig\Core\StateMachines;
+
+use SmashPig\Core\Hook;
+use SmashPig\Core\IStorable;
+
+abstract class AbstractStateMachine
+ implements IStateMachine, IStorable
+{
+ protected $signalQueue = array();
+ protected $state;
+ protected $busy;
+ protected $priority;
+
+ protected $name;
+ protected $description;
+
+ protected $propertiesExcludedFromExport = array(
+ 'busy', 'description'
+ );
+
+ function __construct( StateMachineDescription $desc ) {
+ //TODO: set priority based on config x machine description
+
+ $this->name = $desc->getName();
+ $this->description = $desc;
+ }
+
+ function getAvailableTransitions() {
+ return $this->description->getTransitionsForState( $this->state
);
+ }
+
+ function isBusy() {
+ return $this->busy;
+ }
+
+ function isDormant() {
+ return !($this->busy or $signalQueue);
+ }
+
+ function isFinished() {
+ return empty( $this->getAvailableTransitions() );
+ }
+
+ protected function handleException( Exception $ex ) {
+ // Clear the signal stream, something unexpected happened
+ $signalQueue = array();
+
+ if ( $ex instanceof INamedException ) {
+ $this->runHandler( $ex->getName() );
+ } else {
+ throw $ex;
+ }
+
+ // or override the meaning of, $this->runHandler(
$ex->getCode() );
+ }
+
+ protected function runHandler( $name ) {
+ try {
+ $func = $this->findHandler( $name );
+ if ( !is_callable( $func ) ) {
+ throw new SignalHandlerNotFoundException(
$this->name, $this->state, $name );
+ }
+
+ Hook::run( "{$name}/pre", $this );
+
+ $result = $func();
+
+ Hook::run( "{$name}/post", $this );
+
+ if ( false === $result ) {
+ // This transition has failed. Do not take
default actions.
+ } else {
+ $this->setState(
$this->description->getDestinationState( $this->getState(), $name ) );
+ }
+ } catch ( Exception $ex ) {
+ $this->handleException( $ex );
+ }
+ }
+
+ function signal( $name ) {
+ if ( in_array( $name, $this->getDescription()->getExceptions()
) ) {
+ // If an exception is passed as an ordinary signal,
purge the
+ // signal stream. We are breaking the machine.
+ $this->signalQueue = array()
+ }
+ if ( $this->isBusy() ) {
+ $this->signalQueue[] = $name;
+ return;
+ }
+ switch ( $this->priority ) {
+ case 'realtime':
+ // The signal stream is emptied by this thread, without
relinquishing
+ // control, until all signals have been consumed.
+ while ( $signal = array_shift( $this->signalQueue ) ) {
+ $this->doTransition( $signal );
+ }
+ break;
+ default:
+ $this->doTransition( $name );
+ }
+ }
+
+ protected function doTransition( $name ) {
+ if ( !in_array( $name, $this->getAvailableTransitions() ) ) {
+ // TODO log debug. Although, propagating signals
willy-nilly
+ // doesn't look so promising, if this code is any
measure.
+ return;
+ }
+ $this->busy = true;
+ $this->runHandler( $name );
+ $this->busy = false;
+ }
+
+ function freeze() {
+ $this->busy = true;
+ $this->sync();
+ }
+
+ function sync() {
+ //TODO
+ }
+}
+
+interface IVisibleStateMachine {
+ function getStateName();
+}
+
+interface IContainer {
+ function getChildren();
+}
+
+class SignalHandlerNotFoundException extends Exception {
+ protected $message;
+
+ function __construct( $machine, $state, $name ) {
+ $message = "Transition missing handler:
{$machine}::{$state}->{$name}";
+ }
+
+ function getMessage() {
+ return $message;
+ }
+}
diff --git a/SmashPig/Core/StateMachines/IStateMachine.php
b/SmashPig/Core/StateMachines/IStateMachine.php
new file mode 100644
index 0000000..07ba0b4
--- /dev/null
+++ b/SmashPig/Core/StateMachines/IStateMachine.php
@@ -0,0 +1,26 @@
+<?php
+
+interface IStateMachine {
+ function begin();
+
+ /**
+ * Enqueue a signal, causing a transition to begin
+ */
+ function signal( string $name );
+
+ /**
+ * Return the state machine definition.
+ function getDescription();
+ **/
+
+ /**
+ * Returns all signals leading from the current node, regardless of
+ * whether any preconditions would be met.
+ */
+ function getAvailableTransitions();
+
+ /**
+ * Doing something atomic. If there is a transition or data sync in
progress, returns true.
+ */
+ function isBusy();
+}
diff --git a/SmashPig/Core/StateMachines/NetworkEgress.php
b/SmashPig/Core/StateMachines/NetworkEgress.php
new file mode 100644
index 0000000..73f091b
--- /dev/null
+++ b/SmashPig/Core/StateMachines/NetworkEgress.php
@@ -0,0 +1,62 @@
+<?php namespace SmashPig\Core\StateMachines;
+
+class NetworkEgress extends AbstractStateMachine {
+ protected $context;
+
+ 'states' => array(
+ 'new' => array(
+ 'signals' => array(
+ 'send' => 'wait_response',
+ ),
+ ),
+ 'wait_response' => array(
+ 'signals' => array(
+ 'timeout' => 'backoff',
+ 'received' => 'processing',
+ ),
+ ),
+ 'processing'
+
+ 'signals' => array(
+ 'respond' => 'wait_receipt',
+ ),
+ ),
+ 'wait_receipt' => array(
+ 'signals' => array(
+ 'receipt_received' => 'done',
+ 'timeout' => 'failed',
+ ),
+ ),
+ 'done',
+ 'failed',
+ 'backoff' => array(
+ 'signals' => array(
+ 'retry' => 'wait_response',
+ 'give_up' => 'failed',
+ ),
+ ),
+ ),
+
+ 'exceptions' => array(
+ 'timeout',
+ ),
+
+// XXX pass the signal target data
+class Timeout {
+ static function handle_alarm() {
+ }
+
+ static function set( $seconds, $callback, $params ) {
+ // XXX no good on Windows
+ pcntl_signal( SIGALRM, array( 'Timeout', 'handle_alarm' ), true );
+ // might fail under a webserver
+ set_time_limit( $seconds + 30 );
+ function trigger_signal( $machine, $signal ) {
+ // restart the clock
+ set_time_limit( 30 );
+ }
+
+ function handle_send() {
+ $request = $this->context->getData( 'network/request' );
+ }
+}
diff --git a/SmashPig/Core/StateMachines/StateMachineDescription.php
b/SmashPig/Core/StateMachines/StateMachineDescription.php
new file mode 100644
index 0000000..d53208d
--- /dev/null
+++ b/SmashPig/Core/StateMachines/StateMachineDescription.php
@@ -0,0 +1,16 @@
+<?php namespace SmashPig\Core\StateMachines;
+
+/**
+ */
+class StateMachineDescription {
+
+ /**
+ */
+ function getTransitionsForState( string $state );
+
+ function getDestinationState( string $state, string $transition );
+
+ function getName();
+
+ function getExceptions();
+}
diff --git a/SmashPig/Core/StateMachines/TransactionLifespan.php
b/SmashPig/Core/StateMachines/TransactionLifespan.php
new file mode 100644
index 0000000..2b4f899
--- /dev/null
+++ b/SmashPig/Core/StateMachines/TransactionLifespan.php
@@ -0,0 +1,47 @@
+<?php namespace SmashPig\Core\StateMachines;
+
+/* implements IVisibleStateMachine */
+ 'states' => array(
+ 'new' => array(
+ 'signals' => array(
+ 'authorize' => 'wait_auth',
+ 'charge' => 'wait_charge',
+ ),
+ ),
+ 'wait_auth' => array(
+ 'signals' => array(
+ 'got_authorization' => 'authorized',
+ 'failed_authorization' => 'failed',
+ ),
+ ),
+ 'wait_settled' => array(
+ 'signals' => array(
+ 'got_settlement' => 'completed',
+ ),
+ ),
+ 'completed' => array(
+ 'signals' => array(
+ 'chargeback' => 'chargedback',
+ 'refund' => 'refunded',
+ ),
+ ),
+ 'wait_cancel' => array(
+ ),
+ 'cancelled',
+ 'void',
+ 'chargedback',
+ 'refunded',
+
+ function handle_cancel() {
+ if ( $this->getState() === 'completed' ) {
+ $this->signal( 'refund' );
+ } else {
+ $this->setState() = 'wait_cancel';
+ $this->process( 'cancel' );
+ }
+ }
+
+ 'exceptions' => array(
+ 'cancel',
+ ),
+ 'priority' => 'normal',
diff --git a/SmashPig/Core/StateMachines/TransitionNotPossibleException.php
b/SmashPig/Core/StateMachines/TransitionNotPossibleException.php
new file mode 100644
index 0000000..17422ca
--- /dev/null
+++ b/SmashPig/Core/StateMachines/TransitionNotPossibleException.php
@@ -0,0 +1,3 @@
+<?php namespace SmashPig\Core\StateMachines;
+class TransitionNotPossibleException extends Exception {
+}
diff --git a/SmashPig/Core/Transformations/IProteanData.php
b/SmashPig/Core/Transformations/IProteanData.php
new file mode 100644
index 0000000..3d43200
--- /dev/null
+++ b/SmashPig/Core/Transformations/IProteanData.php
@@ -0,0 +1,14 @@
+<?php namespace SmashPig\Core\Transformations;
+
+/**
+ * A datastore which converts raw data into any one of multiple
+ * alternatives.
+ */
+class IProteanData {
+ function setRawData( $data );
+
+ /**
+ * Run a specific transformation on the data and return it.
+ */
+ function getDataVariant( string $variation );
+}
diff --git a/SmashPig/Core/Transformations/ITransformation.php
b/SmashPig/Core/Transformations/ITransformation.php
new file mode 100644
index 0000000..bf488e8
--- /dev/null
+++ b/SmashPig/Core/Transformations/ITransformation.php
@@ -0,0 +1,13 @@
+<?php namespace SmashPig\Core\Transformations;
+
+/**
+ * Transformations are run explicitly, or through a TransformedData object
+ * which contains input data and provides one or more views which are a
+ * composition of functions.
+ */
+interface ITransformation {
+ /**
+ * Perform a transformation and return the result.
+ */
+ function transform( $data );
+}
diff --git a/SmashPig/Core/Transformations/TransformationChain.php
b/SmashPig/Core/Transformations/TransformationChain.php
new file mode 100644
index 0000000..1ebb3db
--- /dev/null
+++ b/SmashPig/Core/Transformations/TransformationChain.php
@@ -0,0 +1,19 @@
+<?php namespace SmashPig\Core\Transformations;
+
+class TransformationChain implements ITransformation {
+ protected $transformations = array();
+
+ function appendTransform( ITransformation $transformation ) {
+ $transformations[] = $transformation;
+ }
+
+ function transform( $data ) {
+ return array_reduce(
+ $transformations,
+ function ( $result, $transformation ) {
+ return $transformation->transform( $result );
+ },
+ $data
+ );
+ }
+}
diff --git a/SmashPig/Core/Transformations/TransformedData.php
b/SmashPig/Core/Transformations/TransformedData.php
new file mode 100644
index 0000000..314f133
--- /dev/null
+++ b/SmashPig/Core/Transformations/TransformedData.php
@@ -0,0 +1,26 @@
+<?php namespace SmashPig\Core\Transformations;
+
+/**
+ * A collection of views on data, provided by transformations
+ */
+class TransformedData implements IProteanData {
+ protected /* & */$rawData;
+ protected $variations = array();
+
+ function setRawData( $data ) {
+ // The reference is taken to keep up-to-date
+ $this->rawData = &$data;
+ }
+
+ function addVariant( string $name, ITransformation $transformation ) {
+ $variations[$name] = $provider;
+ }
+
+ function getDataVariant( string $name ) {
+ if ( !array_key_exists( $variation, $variations ) ) {
+ return array();
+ }
+ $transformation = $variations[$name];
+ return $transformation->transform( $this->rawData );
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/52167
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I9ce59073496b6baf22c5f5eb2698753cbbe3780d
Gerrit-PatchSet: 1
Gerrit-Project: wikimedia/fundraising/PaymentsListeners
Gerrit-Branch: master
Gerrit-Owner: Adamw <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits