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

Reply via email to