Rewrite JS DemandMatcher to handle important latency-related corner cases.
This commit is contained in:
parent
68cde5be6c
commit
628ba87c54
|
@ -0,0 +1,57 @@
|
||||||
|
digraph G {
|
||||||
|
node[shape=box];
|
||||||
|
|
||||||
|
// s0000 idle
|
||||||
|
// s1000 error
|
||||||
|
// s0100 supply
|
||||||
|
// s1100 running
|
||||||
|
// s1010 starting
|
||||||
|
// s0011 starting_unwanted
|
||||||
|
// s1011 starting_doomed
|
||||||
|
// s0101 unwanted
|
||||||
|
// s1101 running_doomed
|
||||||
|
|
||||||
|
idle -> starting [label="D+/start"];
|
||||||
|
supply -> starting [label="D+,S-/start"];
|
||||||
|
error -> idle [label="D-"];
|
||||||
|
error -> running [label="S+"];
|
||||||
|
error -> unwanted [label="D-,S+"];
|
||||||
|
running -> unwanted [label="D-"];
|
||||||
|
running -> error [label="S-/error"];
|
||||||
|
|
||||||
|
unwanted -> idle [label="S-"];
|
||||||
|
unwanted -> starting [label="D+,S-/start"];
|
||||||
|
running_doomed -> starting [label="S-/start"];
|
||||||
|
running_doomed -> idle [label="D-,S-"];
|
||||||
|
|
||||||
|
starting -> starting_unwanted [label="D-"];
|
||||||
|
starting -> running [label="S+"];
|
||||||
|
starting -> unwanted [label="D-,S+"];
|
||||||
|
starting_unwanted -> unwanted [label="S+"];
|
||||||
|
starting_unwanted -> running_doomed [label="D+,S+"];
|
||||||
|
starting_doomed -> running_doomed [label="S+"];
|
||||||
|
starting_doomed -> unwanted [label="D-,S+"];
|
||||||
|
|
||||||
|
|
||||||
|
idle -> supply [label="S+"];
|
||||||
|
idle -> running [label="D+S+"];
|
||||||
|
supply -> running [label="D+"];
|
||||||
|
supply -> idle [label="S-"];
|
||||||
|
running -> idle [label="D-,S-"];
|
||||||
|
unwanted -> running_doomed [label="D+"];
|
||||||
|
running_doomed -> unwanted [label="D-"];
|
||||||
|
starting_unwanted -> starting_doomed [label="D+"];
|
||||||
|
starting_doomed -> starting_unwanted [label="D-"];
|
||||||
|
|
||||||
|
|
||||||
|
// s0001 -> impossible [label="any"];
|
||||||
|
// s0010 -> impossible [label="any"];
|
||||||
|
|
||||||
|
// s1001 -> impossible [label="any"];
|
||||||
|
|
||||||
|
// s0110 -> impossible [label="any"];
|
||||||
|
// s1110 -> impossible [label="any"];
|
||||||
|
// s0111 -> impossible [label="any"];
|
||||||
|
// s1111 -> impossible [label="any"];
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,376 @@
|
||||||
|
# Demand-matching and Supervision
|
||||||
|
|
||||||
|
The Demand Matcher pattern (in `demand-matcher.rkt` and in
|
||||||
|
`demand-matcher.js`'s `DemandMatcher` class) tracks assertions
|
||||||
|
representing some abstract *demand* for a resource, and causes the
|
||||||
|
creation or acquisition of matching *supply* of that resource.
|
||||||
|
|
||||||
|
To do this, it tracks the state of each *instance* of the resource.
|
||||||
|
Each resource instance (called a "task") is uniquely identified by a
|
||||||
|
projection of the dataspace.
|
||||||
|
|
||||||
|
The basic idea is that:
|
||||||
|
|
||||||
|
- When demand for a task is detected, it is started.
|
||||||
|
|
||||||
|
- Each started task signals its presence to the DemandMatcher.
|
||||||
|
|
||||||
|
- When demand drops, the task should detect this and exit.
|
||||||
|
|
||||||
|
- If the task exits unexpectedly, this is an error, and the
|
||||||
|
DemandMatcher prints a warning.
|
||||||
|
|
||||||
|
## Latency causes problems
|
||||||
|
|
||||||
|
However, because there can be some latency between requesting the
|
||||||
|
start of a task and its signalling its presence to the DemandMatcher,
|
||||||
|
we can't just figure out what to do based on the presence or absence
|
||||||
|
of demand and supply for a task. We also need to track a few more bits
|
||||||
|
of information.
|
||||||
|
|
||||||
|
When demand for a task drops briefly, we *expect* a drop in supply,
|
||||||
|
*even demand increases again before we detect a supply drop*. For this
|
||||||
|
reason, in some circumstances, the default task supervision strategy
|
||||||
|
of DemandMatcher *recreates* supply on supply drop in some
|
||||||
|
circumstances. It keeps track of whether a supply increase is
|
||||||
|
expected, and of whether a supply decrease is expected for each task.
|
||||||
|
|
||||||
|
It becomes an important part of the DemandMatcher protocol for a task
|
||||||
|
instance to always drop its supply assertion in response to a drop in
|
||||||
|
demand. This works well in Syndicate implementations that preserve all
|
||||||
|
assertion transitions, but not at all well where brief transitions may
|
||||||
|
be elided. In those cases, we will have to reach for a more heuristic
|
||||||
|
approach involving something akin to Erlang's "Maximum Restart
|
||||||
|
Intensity" and/or other kinds of time-based decision. For now though,
|
||||||
|
the precise case works fine.
|
||||||
|
|
||||||
|
While it seems simple enough to imagine, the details are rather
|
||||||
|
fiddly.
|
||||||
|
|
||||||
|
## Working out the algorithm that defaultTaskSupervisor should use
|
||||||
|
|
||||||
|
We may assume some expected task behaviour: that it will eventually
|
||||||
|
assert supply, and *then* upon demand drop eventually exit.
|
||||||
|
|
||||||
|
◇(supply ∧ (¬demand ⇒ ◇ terminate)) (?!?!)
|
||||||
|
|
||||||
|
### Complete table of actions
|
||||||
|
|
||||||
|
Each row in this table describes actions taken in a particular
|
||||||
|
circumstance by `defaultTaskSupervisor`.
|
||||||
|
|
||||||
|
The table has seven columns:
|
||||||
|
- `∃D`, whether demand for the task exists currently
|
||||||
|
- `∃S`, whether supply for the task exists currently
|
||||||
|
- `ΔD`, whether (and in which direction) demand is changing now
|
||||||
|
- `ΔS`, whether (and in which direction) supply is changing now
|
||||||
|
- `expS+`, whether we expect a supply increase at some point in future
|
||||||
|
- `expS-`, whether we expect a supply decrease at some point in future
|
||||||
|
- and an action to take in this circumstance.
|
||||||
|
|
||||||
|
The first two values are drawn from the state of the DemandMatcher;
|
||||||
|
the second two, from the patch event the DemandMatcher is currently
|
||||||
|
processing; and the third two are private state variables of the task
|
||||||
|
supervisor itself.
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS- Action
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
- - + - - Start task, set expS+
|
||||||
|
- - + - - No action (but slightly weird)
|
||||||
|
- - + + - - No action (but slightly weird)
|
||||||
|
- Y + - - No action (pre-extant supply)
|
||||||
|
- Y - - - No action
|
||||||
|
- Y + - - - Start task, set expS+
|
||||||
|
Y - - - - Demand goes after unexpected supply drop
|
||||||
|
Y - + - - Spontaneous recovery from unexpected supply drop
|
||||||
|
Y - - + - - Spontaneous recovery from unexpected supply drop; set expS-
|
||||||
|
Y Y - - - Set expS-
|
||||||
|
Y Y - - - Unexpected supply drop error
|
||||||
|
Y Y - - - - No action (but slightly weird)
|
||||||
|
|
||||||
|
- - + - Y Impossible (expS- would be clear or expS+ set)
|
||||||
|
- - + - Y Impossible (expS- would be clear or expS+ set)
|
||||||
|
- - + + - Y Impossible (expS- would be clear or expS+ set)
|
||||||
|
- Y + - Y No action
|
||||||
|
- Y - - Y Clear expS-
|
||||||
|
- Y + - - Y Clear expS-, start task, set expS+
|
||||||
|
Y - - - Y Impossible (expS+ would be set)
|
||||||
|
Y - + - Y Impossible (expS+ would be set)
|
||||||
|
Y - - + - Y Impossible (expS+ would be set)
|
||||||
|
Y Y - - Y No action
|
||||||
|
Y Y - - Y Clear expS-, start task, set expS+
|
||||||
|
Y Y - - - Y Clear expS-
|
||||||
|
|
||||||
|
- - + Y - Impossible (expS+ would be clear or expS- set)
|
||||||
|
- - + Y - Impossible (expS+ would be clear or expS- set)
|
||||||
|
- - + + Y - Impossible (expS+ would be clear or expS- set)
|
||||||
|
- Y + Y - Impossible (expS+ would be clear)
|
||||||
|
- Y - Y - Impossible (expS+ would be clear)
|
||||||
|
- Y + - Y - Impossible (expS+ would be clear)
|
||||||
|
Y - - Y - Set expS-
|
||||||
|
Y - + Y - Clear expS+
|
||||||
|
Y - - + Y - Clear expS+, set expS-
|
||||||
|
Y Y - Y - Impossible (expS+ would be clear)
|
||||||
|
Y Y - Y - Impossible (expS+ would be clear)
|
||||||
|
Y Y - - Y - Impossible (expS+ would be clear)
|
||||||
|
|
||||||
|
- - + Y Y No action
|
||||||
|
- - + Y Y Clear expS+
|
||||||
|
- - + + Y Y Clear expS+
|
||||||
|
- Y + Y Y Impossible (expS+ would be clear)
|
||||||
|
- Y - Y Y Impossible (expS+ would be clear)
|
||||||
|
- Y + - Y Y Impossible (expS+ would be clear)
|
||||||
|
Y - - Y Y No action
|
||||||
|
Y - + Y Y Clear expS+
|
||||||
|
Y - - + Y Y Clear expS+
|
||||||
|
Y Y - Y Y Impossible (expS+ would be clear)
|
||||||
|
Y Y - Y Y Impossible (expS+ would be clear)
|
||||||
|
Y Y - - Y Y Impossible (expS+ would be clear)
|
||||||
|
|
||||||
|
#### Actions and transitions involving actions
|
||||||
|
|
||||||
|
From the table, we learn that the possible actions are:
|
||||||
|
|
||||||
|
- `START`, Start task, set expS+
|
||||||
|
- `EXPDROP`, Set expS-
|
||||||
|
- `GOTDROP`, Clear expS-
|
||||||
|
- `RUNNING`, Clear expS+
|
||||||
|
|
||||||
|
There are also a couple of pseudo-actions, `ERROR` for an unexpected
|
||||||
|
supply drop, and `RECOVER` for circumstances marking spontaneous
|
||||||
|
recovery after an unexpected supply drop.
|
||||||
|
|
||||||
|
The final four columns in this table are the new states of the
|
||||||
|
DemandMatcher and the task supervisor.
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS- Actions Next: ∃D ∃S expS+ expS-
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
- - + - - START Y - Y -
|
||||||
|
- Y + - - - START Y - Y -
|
||||||
|
Y - - - - RECOVERY - - - -
|
||||||
|
Y - + - - RECOVER Y Y - -
|
||||||
|
Y - - + - - RECOVER EXPDROP - Y - Y
|
||||||
|
Y Y - - - EXPDROP - Y - Y
|
||||||
|
Y Y - - - ERROR Y - - -
|
||||||
|
|
||||||
|
- Y - - Y GOTDROP - - - -
|
||||||
|
- Y + - - Y GOTDROP START Y - Y -
|
||||||
|
Y Y - - Y GOTDROP START Y - Y -
|
||||||
|
Y Y - - - Y GOTDROP - - - -
|
||||||
|
|
||||||
|
Y - - Y - EXPDROP - - Y Y
|
||||||
|
Y - + Y - RUNNING Y Y - -
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
|
||||||
|
- - + Y Y RUNNING - Y - Y
|
||||||
|
- - + + Y Y RUNNING Y Y - Y
|
||||||
|
Y - + Y Y RUNNING Y Y - Y
|
||||||
|
Y - - + Y Y RUNNING - Y - Y
|
||||||
|
|
||||||
|
#### Impossible states
|
||||||
|
|
||||||
|
Some states are impossible to reach.
|
||||||
|
|
||||||
|
It is impossible for neither supply nor demand to exist, when either
|
||||||
|
but not both of a rise or a drop in supply is expected:
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS-
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
- - - Y Impossible (expS- would be clear or expS+ set)
|
||||||
|
- - Y - Impossible (expS+ would be clear or expS- set)
|
||||||
|
|
||||||
|
It is impossible for demand but no supply to exist, when a drop in
|
||||||
|
supply is expected but no rise in supply is expected:
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS-
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
Y - - Y Impossible (expS+ would be set)
|
||||||
|
|
||||||
|
It is impossible for supply to exist while a rise in supply is
|
||||||
|
expected:
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS-
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
- Y Y - Impossible (expS+ would be clear)
|
||||||
|
Y Y Y - Impossible (expS+ would be clear)
|
||||||
|
- Y Y Y Impossible (expS+ would be clear)
|
||||||
|
Y Y Y Y Impossible (expS+ would be clear)
|
||||||
|
|
||||||
|
#### Transitions involving only DemandMatcher state change
|
||||||
|
|
||||||
|
Where no task supervisor state changes and no actions are needed:
|
||||||
|
|
||||||
|
∃D ∃S ΔD ΔS expS+ expS- Actions Next: ∃D ∃S expS+ expS-
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
- - + - - - Y - -
|
||||||
|
- - + + - - Y Y - -
|
||||||
|
- Y + - - Y Y - -
|
||||||
|
- Y - - - - - - -
|
||||||
|
Y Y - - - - - - - -
|
||||||
|
- Y + - Y Y Y - Y
|
||||||
|
Y Y - - Y - Y - Y
|
||||||
|
- - + Y Y Y - Y Y
|
||||||
|
Y - - Y Y - - Y Y
|
||||||
|
|
||||||
|
### Transition diagram
|
||||||
|
|
||||||
|
![DemandMatcher task supervisor transition diagram](demand-matcher.png)
|
||||||
|
|
||||||
|
### From state machine to implementation
|
||||||
|
|
||||||
|
We can give the reachable states reasonable names:
|
||||||
|
|
||||||
|
∃D ∃S expS+ expS- Name
|
||||||
|
---------------------------------------
|
||||||
|
- - - - IDLE
|
||||||
|
Y - Y - STARTING
|
||||||
|
Y Y - - RUNNING
|
||||||
|
- Y - Y UNWANTED
|
||||||
|
|
||||||
|
Y - Y Y STARTING_DOOMED
|
||||||
|
- - Y Y STARTING_UNWANTED
|
||||||
|
Y Y - Y RUNNING_DOOMED
|
||||||
|
|
||||||
|
- Y - - SUPPLY
|
||||||
|
Y - - - ERROR
|
||||||
|
|
||||||
|
However, writing out the full state machine in terms of these states
|
||||||
|
doesn't exploit all the redundancy in the machine.
|
||||||
|
|
||||||
|
Instead, let's group transitions by their effects on the task
|
||||||
|
supervisor's state, the "expected" bits. There are only four possible
|
||||||
|
actions (excluding warnings related to recovery etc.):
|
||||||
|
|
||||||
|
START - set expS+ (and start a task instance)
|
||||||
|
RUNNING - clear expS+
|
||||||
|
EXPDROP - set expS-
|
||||||
|
GOTDROP - clear expS-
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Leave expS+ alone, set expS-:
|
||||||
|
Y - - + - - EXPDROP - Y - Y
|
||||||
|
Y Y - - - EXPDROP - Y - Y
|
||||||
|
Y - - Y - EXPDROP - - Y Y
|
||||||
|
|
||||||
|
Leave expS+ alone, clear expS-:
|
||||||
|
- Y - - Y GOTDROP - - - -
|
||||||
|
Y Y - - - Y GOTDROP - - - -
|
||||||
|
|
||||||
|
Set expS+, leave expS- alone:
|
||||||
|
- - + - - START Y - Y -
|
||||||
|
- Y + - - - START Y - Y -
|
||||||
|
|
||||||
|
Set expS+, clear expS-:
|
||||||
|
- Y + - - Y START GOTDROP Y - Y -
|
||||||
|
Y Y - - Y START GOTDROP Y - Y -
|
||||||
|
|
||||||
|
Clear expS+, leave expS- alone:
|
||||||
|
Y - + Y - RUNNING Y Y - -
|
||||||
|
- - + Y Y RUNNING - Y - Y
|
||||||
|
- - + + Y Y RUNNING Y Y - Y
|
||||||
|
Y - + Y Y RUNNING Y Y - Y
|
||||||
|
Y - - + Y Y RUNNING - Y - Y
|
||||||
|
|
||||||
|
Clear expS+, set expS-:
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Now, let's look at those grouped by specific action (some rows will
|
||||||
|
appear twice, because some rows involve more than one action):
|
||||||
|
|
||||||
|
Expdrop:
|
||||||
|
Y - - + - - EXPDROP - Y - Y
|
||||||
|
Y Y - - - EXPDROP - Y - Y
|
||||||
|
Y - - Y - EXPDROP - - Y Y
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
|
||||||
|
- "Set expS- whenever a drop in demand is detected, and either (a)
|
||||||
|
increase in supply is detected, (b) supply exists and is not
|
||||||
|
falling, or (c) supply is expected to exist."
|
||||||
|
|
||||||
|
Gotdrop:
|
||||||
|
- Y - - Y GOTDROP - - - -
|
||||||
|
Y Y - - - Y GOTDROP - - - -
|
||||||
|
- Y + - - Y START GOTDROP Y - Y -
|
||||||
|
Y Y - - Y START GOTDROP Y - Y -
|
||||||
|
|
||||||
|
- "Clear expS- whenever a drop in supply is detected."
|
||||||
|
|
||||||
|
Start:
|
||||||
|
- - + - - START Y - Y -
|
||||||
|
- Y + - - - START Y - Y -
|
||||||
|
- Y + - - Y START GOTDROP Y - Y -
|
||||||
|
Y Y - - Y START GOTDROP Y - Y -
|
||||||
|
|
||||||
|
- "Set expS+ and start a task whenever expS+ is clear and demand
|
||||||
|
becomes or remains high and supply becomes or remains low UNLESS
|
||||||
|
demand is already high, supply drops, and expS- is clear, which is
|
||||||
|
the 'unexpected drop' error case."
|
||||||
|
|
||||||
|
Running:
|
||||||
|
Y - + Y - RUNNING Y Y - -
|
||||||
|
- - + Y Y RUNNING - Y - Y
|
||||||
|
- - + + Y Y RUNNING Y Y - Y
|
||||||
|
Y - + Y Y RUNNING Y Y - Y
|
||||||
|
Y - - + Y Y RUNNING - Y - Y
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
|
||||||
|
- "Clear expS+ whenever supply increases."
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Now let's take those rules and check them against the full rulebase:
|
||||||
|
|
||||||
|
"Set expS- whenever a drop in demand is detected, and either (a)
|
||||||
|
increase in supply is detected, (b) supply exists and is not
|
||||||
|
falling, or (c) supply is expected to exist."
|
||||||
|
|
||||||
|
Y - - + - - RECOVER EXPDROP - Y - Y
|
||||||
|
Y Y - - - EXPDROP - Y - Y
|
||||||
|
Y - - Y - EXPDROP - - Y Y
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
Y - - + Y Y RUNNING - Y - Y
|
||||||
|
Y Y - - Y - Y - Y
|
||||||
|
Y - - Y Y - - Y Y
|
||||||
|
|
||||||
|
"Clear expS- whenever a drop in supply is detected."
|
||||||
|
|
||||||
|
- Y + - - - START Y - Y -
|
||||||
|
Y Y - - - ERROR Y - - -
|
||||||
|
- Y - - Y GOTDROP - - - -
|
||||||
|
- Y + - - Y GOTDROP START Y - Y -
|
||||||
|
Y Y - - Y GOTDROP START Y - Y -
|
||||||
|
Y Y - - - Y GOTDROP - - - -
|
||||||
|
- Y - - - - - - -
|
||||||
|
Y Y - - - - - - - -
|
||||||
|
|
||||||
|
"Set expS+ and start a task whenever expS+ is clear and demand
|
||||||
|
becomes or remains high and supply becomes or remains low UNLESS
|
||||||
|
demand is already high, supply drops, and expS- is clear, which is
|
||||||
|
the 'unexpected drop' error case."
|
||||||
|
|
||||||
|
- - + - - START Y - Y -
|
||||||
|
- Y + - - - START Y - Y -
|
||||||
|
Y Y - - - ERROR Y - - -
|
||||||
|
- Y + - - Y GOTDROP START Y - Y -
|
||||||
|
Y Y - - Y GOTDROP START Y - Y -
|
||||||
|
|
||||||
|
"Clear expS+ whenever supply increases."
|
||||||
|
|
||||||
|
Y - + - - RECOVER Y Y - -
|
||||||
|
Y - - + - - RECOVER EXPDROP - Y - Y
|
||||||
|
Y - + Y - RUNNING Y Y - -
|
||||||
|
Y - - + Y - RUNNING EXPDROP - Y - Y
|
||||||
|
- - + Y Y RUNNING - Y - Y
|
||||||
|
- - + + Y Y RUNNING Y Y - Y
|
||||||
|
Y - + Y Y RUNNING Y Y - Y
|
||||||
|
Y - - + Y Y RUNNING - Y - Y
|
||||||
|
- - + - - - Y - -
|
||||||
|
- - + + - - Y Y - -
|
||||||
|
|
||||||
|
By looking at the next-state columns corresponding to the action
|
||||||
|
described, we can see that each predicate used to decide whether to
|
||||||
|
set or clear each state bit is a sound overapproximation of the
|
||||||
|
behaviour we want.
|
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
|
@ -31,12 +31,12 @@ function spawnBrokerClientDriver() {
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([brokerConnection(URL)],
|
new DemandMatcher([brokerConnection(URL)],
|
||||||
[brokerConnection(URL)],
|
[brokerConnection(URL)],
|
||||||
|
function (c) {
|
||||||
|
Dataspace.spawn(new BrokerClientConnection(c.url));
|
||||||
|
},
|
||||||
{
|
{
|
||||||
demandMetaLevel: 1,
|
demandMetaLevel: 1,
|
||||||
supplyMetaLevel: 0,
|
supplyMetaLevel: 0
|
||||||
onDemandIncrease: function (c) {
|
|
||||||
Dataspace.spawn(new BrokerClientConnection(c.url));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,100 @@ var Trie = require('./trie.js');
|
||||||
var Patch = require('./patch.js');
|
var Patch = require('./patch.js');
|
||||||
var Util = require('./util.js');
|
var Util = require('./util.js');
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protocol between DemandMatcher and taskSupervisor functions
|
||||||
|
|
||||||
|
// Bits:
|
||||||
|
var IS_CHANGING = 1;
|
||||||
|
var IS_PRESENT = 2;
|
||||||
|
|
||||||
|
// Bit combinations:
|
||||||
|
var LOW = 0 ;
|
||||||
|
var RISING = IS_CHANGING ;
|
||||||
|
var HIGH = IS_PRESENT ;
|
||||||
|
var FALLING = IS_PRESENT | IS_CHANGING ;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Default task supervision strategy. See syndicate/doc/demand-matcher.md.
|
||||||
|
|
||||||
|
function defaultTaskSupervisor(demandState, supplyState, supervisionState, taskFn, errorFn) {
|
||||||
|
var oldESI = supervisionState ? supervisionState.expectSupplyIncrease : false;
|
||||||
|
var oldESD = supervisionState ? supervisionState.expectSupplyDecrease : false;
|
||||||
|
|
||||||
|
var newESI = oldESI;
|
||||||
|
var newESD = oldESD;
|
||||||
|
|
||||||
|
if ((demandState === FALLING) && ((supplyState === RISING) ||
|
||||||
|
(supplyState === HIGH) ||
|
||||||
|
oldESI)) {
|
||||||
|
newESD = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldESI && ((demandState === RISING) ||
|
||||||
|
(demandState === HIGH)) && ((supplyState === LOW) ||
|
||||||
|
(supplyState === FALLING))) {
|
||||||
|
if ((demandState === HIGH) && !oldESD) {
|
||||||
|
errorFn("Syndicate: DemandMatcher detected unexpected drop in supply");
|
||||||
|
} else {
|
||||||
|
taskFn();
|
||||||
|
newESI = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supplyState === FALLING) newESD = false;
|
||||||
|
if (supplyState === RISING) newESI = false;
|
||||||
|
|
||||||
|
if (newESI || newESD) {
|
||||||
|
return { expectSupplyIncrease: newESI, expectSupplyDecrease: newESD };
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// DemandMatcher itself
|
||||||
|
|
||||||
|
function DemandMatcher(demandSpecs, supplySpecs, startTask, options) {
|
||||||
|
options = Util.extend({
|
||||||
|
metaLevel: 0,
|
||||||
|
demandMetaLevel: null,
|
||||||
|
supplyMetaLevel: null,
|
||||||
|
taskSupervisor: defaultTaskSupervisor,
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
if (typeof startTask !== 'function') {
|
||||||
|
throw new Error("Syndicate: DemandMatcher expects 'startTask' function as third argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.demandProjectionNames = ensureMatchingProjectionNames(demandSpecs);
|
||||||
|
this.supplyProjectionNames = ensureMatchingProjectionNames(supplySpecs);
|
||||||
|
ensureMatchingProjectionNames([demandSpecs[0], supplySpecs[0]]);
|
||||||
|
|
||||||
|
this.demandSpecs = demandSpecs;
|
||||||
|
this.supplySpecs = supplySpecs;
|
||||||
|
|
||||||
|
this.demandPatterns = demandSpecs.map(function (s) { return Trie.projectionToPattern(s); });
|
||||||
|
this.supplyPatterns = supplySpecs.map(function (s) { return Trie.projectionToPattern(s); });
|
||||||
|
|
||||||
|
this.demandMetaLevel =
|
||||||
|
(options.demandMetaLevel === null) ? options.metaLevel : options.demandMetaLevel;
|
||||||
|
this.supplyMetaLevel =
|
||||||
|
(options.supplyMetaLevel === null) ? options.metaLevel : options.supplyMetaLevel;
|
||||||
|
|
||||||
|
function metaWrap(n) {
|
||||||
|
return function (s) { return Patch.prependAtMeta(s, n); };
|
||||||
|
}
|
||||||
|
this.demandProjections = demandSpecs.map(metaWrap(this.demandMetaLevel));
|
||||||
|
this.supplyProjections = supplySpecs.map(metaWrap(this.supplyMetaLevel));
|
||||||
|
|
||||||
|
this.taskSupervisor = options.taskSupervisor;
|
||||||
|
this.startTask = startTask;
|
||||||
|
|
||||||
|
this.currentDemand = Immutable.Set();
|
||||||
|
this.currentSupply = Immutable.Set();
|
||||||
|
this.supervisionStates = Immutable.Map();
|
||||||
|
}
|
||||||
|
|
||||||
function ensureMatchingProjectionNames(specs) {
|
function ensureMatchingProjectionNames(specs) {
|
||||||
if (!(specs.length > 0)) {
|
if (!(specs.length > 0)) {
|
||||||
throw new Error("Syndicate: DemandMatcher needs at least one spec");
|
throw new Error("Syndicate: DemandMatcher needs at least one spec");
|
||||||
|
@ -21,52 +115,6 @@ function ensureMatchingProjectionNames(specs) {
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultHandler(side, movement) {
|
|
||||||
return function (captures) {
|
|
||||||
console.error("Syndicate: Unhandled "+movement+" in "+side, captures);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function DemandMatcher(demandSpecs, supplySpecs, options) {
|
|
||||||
options = Util.extend({
|
|
||||||
metaLevel: 0,
|
|
||||||
demandMetaLevel: null,
|
|
||||||
supplyMetaLevel: null,
|
|
||||||
onDemandIncrease: defaultHandler('demand', 'increase'),
|
|
||||||
onDemandDecrease: function (captures) {},
|
|
||||||
onSupplyIncrease: function (captures) {},
|
|
||||||
onSupplyDecrease: defaultHandler('supply', 'decrease')
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
this.demandProjectionNames = ensureMatchingProjectionNames(demandSpecs);
|
|
||||||
this.supplyProjectionNames = ensureMatchingProjectionNames(supplySpecs);
|
|
||||||
|
|
||||||
this.demandSpecs = demandSpecs;
|
|
||||||
this.supplySpecs = supplySpecs;
|
|
||||||
|
|
||||||
this.demandPatterns = demandSpecs.map(function (s) { return Trie.projectionToPattern(s); });
|
|
||||||
this.supplyPatterns = supplySpecs.map(function (s) { return Trie.projectionToPattern(s); });
|
|
||||||
|
|
||||||
this.demandMetaLevel =
|
|
||||||
(options.demandMetaLevel === null) ? options.metaLevel : options.demandMetaLevel;
|
|
||||||
this.supplyMetaLevel =
|
|
||||||
(options.supplyMetaLevel === null) ? options.metaLevel : options.supplyMetaLevel;
|
|
||||||
|
|
||||||
function metaWrap(n) {
|
|
||||||
return function (s) { return Patch.prependAtMeta(s, n); };
|
|
||||||
}
|
|
||||||
this.demandProjections = demandSpecs.map(metaWrap(this.demandMetaLevel));
|
|
||||||
this.supplyProjections = supplySpecs.map(metaWrap(this.supplyMetaLevel));
|
|
||||||
|
|
||||||
this.onDemandIncrease = options.onDemandIncrease;
|
|
||||||
this.onDemandDecrease = options.onDemandDecrease;
|
|
||||||
this.onSupplyIncrease = options.onSupplyIncrease;
|
|
||||||
this.onSupplyDecrease = options.onSupplyDecrease;
|
|
||||||
|
|
||||||
this.currentDemand = Immutable.Set();
|
|
||||||
this.currentSupply = Immutable.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
DemandMatcher.prototype.boot = function () {
|
DemandMatcher.prototype.boot = function () {
|
||||||
var p = Patch.emptyPatch;
|
var p = Patch.emptyPatch;
|
||||||
function extend(ml) {
|
function extend(ml) {
|
||||||
|
@ -83,6 +131,50 @@ DemandMatcher.prototype.handleEvent = function (e) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DemandMatcher.prototype.handlePatch = function (p) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var dN = self.demandProjectionNames.length;
|
||||||
|
var sN = self.supplyProjectionNames.length;
|
||||||
|
var addedDemand = self.extractKeys(p.added, self.demandProjections, dN, 'demand');
|
||||||
|
var removedDemand = self.extractKeys(p.removed, self.demandProjections, dN, 'demand');
|
||||||
|
var addedSupply = self.extractKeys(p.added, self.supplyProjections, sN, 'supply');
|
||||||
|
var removedSupply = self.extractKeys(p.removed, self.supplyProjections, sN, 'supply');
|
||||||
|
|
||||||
|
// Though the added and removed sets of patches are always disjoint,
|
||||||
|
// *after projection* this may not hold. Cancel out any overlaps.
|
||||||
|
var demandOverlap = addedDemand.intersect(removedDemand);
|
||||||
|
var supplyOverlap = addedSupply.intersect(removedSupply);
|
||||||
|
addedDemand = addedDemand.subtract(demandOverlap);
|
||||||
|
removedDemand = removedDemand.subtract(demandOverlap);
|
||||||
|
addedSupply = addedSupply.subtract(supplyOverlap);
|
||||||
|
removedSupply = removedSupply.subtract(supplyOverlap);
|
||||||
|
|
||||||
|
var allTasks = addedDemand.union(addedSupply).union(removedDemand).union(removedSupply);
|
||||||
|
|
||||||
|
allTasks.forEach(function (captures) {
|
||||||
|
function taskFn() {
|
||||||
|
self.startTask(Trie.captureToObject(captures, self.demandProjectionNames));
|
||||||
|
}
|
||||||
|
function errorFn(msg) {
|
||||||
|
console.error(msg, captures);
|
||||||
|
}
|
||||||
|
|
||||||
|
var demandState = computeState(self.currentDemand, addedDemand, removedDemand, captures);
|
||||||
|
var supplyState = computeState(self.currentSupply, addedSupply, removedSupply, captures);
|
||||||
|
var oldSupervisionState = self.supervisionStates.get(captures, null);
|
||||||
|
var newSupervisionState = self.taskSupervisor(demandState,
|
||||||
|
supplyState,
|
||||||
|
oldSupervisionState,
|
||||||
|
taskFn,
|
||||||
|
errorFn);
|
||||||
|
self.supervisionStates = self.supervisionStates.set(captures, newSupervisionState);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.currentSupply = self.currentSupply.union(addedSupply).subtract(removedSupply);
|
||||||
|
self.currentDemand = self.currentDemand.union(addedDemand).subtract(removedDemand);
|
||||||
|
};
|
||||||
|
|
||||||
DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whichSide) {
|
DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whichSide) {
|
||||||
var ks = Immutable.Set();
|
var ks = Immutable.Set();
|
||||||
projections.forEach(function (proj) {
|
projections.forEach(function (proj) {
|
||||||
|
@ -97,53 +189,11 @@ DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whi
|
||||||
return ks;
|
return ks;
|
||||||
};
|
};
|
||||||
|
|
||||||
DemandMatcher.prototype.handlePatch = function (p) {
|
function computeState(current, added, removed, captures) {
|
||||||
var self = this;
|
var isPresent = current.has(captures);
|
||||||
|
var isChanging = added.has(captures) || removed.has(captures);
|
||||||
var dN = self.demandProjectionNames.length;
|
return (isPresent ? IS_PRESENT : 0) | (isChanging ? IS_CHANGING : 0);
|
||||||
var sN = self.supplyProjectionNames.length;
|
}
|
||||||
var addedDemand = this.extractKeys(p.added, self.demandProjections, dN, 'demand');
|
|
||||||
var removedDemand = this.extractKeys(p.removed, self.demandProjections, dN, 'demand');
|
|
||||||
var addedSupply = this.extractKeys(p.added, self.supplyProjections, sN, 'supply');
|
|
||||||
var removedSupply = this.extractKeys(p.removed, self.supplyProjections, sN, 'supply');
|
|
||||||
|
|
||||||
// Though the added and removed sets of patches are always disjoint,
|
|
||||||
// *after projection* this may not hold. Cancel out any overlaps.
|
|
||||||
var demandOverlap = addedDemand.intersect(removedDemand);
|
|
||||||
var supplyOverlap = addedSupply.intersect(removedSupply);
|
|
||||||
addedDemand = addedDemand.subtract(demandOverlap);
|
|
||||||
removedDemand = removedDemand.subtract(demandOverlap);
|
|
||||||
addedSupply = addedSupply.subtract(supplyOverlap);
|
|
||||||
removedSupply = removedSupply.subtract(supplyOverlap);
|
|
||||||
|
|
||||||
self.currentSupply = self.currentSupply.union(addedSupply);
|
|
||||||
self.currentDemand = self.currentDemand.subtract(removedDemand);
|
|
||||||
|
|
||||||
removedSupply.forEach(function (captures) {
|
|
||||||
if (self.currentDemand.has(captures)) {
|
|
||||||
self.onSupplyDecrease(Trie.captureToObject(captures, self.supplyProjectionNames));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addedSupply.forEach(function (captures) {
|
|
||||||
if (!self.currentDemand.has(captures)) {
|
|
||||||
self.onSupplyIncrease(Trie.captureToObject(captures, self.supplyProjectionNames));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
removedDemand.forEach(function (captures) {
|
|
||||||
if (self.currentSupply.has(captures)) {
|
|
||||||
self.onDemandDecrease(Trie.captureToObject(captures, self.demandProjectionNames));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addedDemand.forEach(function (captures) {
|
|
||||||
if (!self.currentSupply.has(captures)) {
|
|
||||||
self.onDemandIncrease(Trie.captureToObject(captures, self.demandProjectionNames));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.currentSupply = self.currentSupply.subtract(removedSupply);
|
|
||||||
self.currentDemand = self.currentDemand.union(addedDemand);
|
|
||||||
};
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
|
@ -15,18 +15,14 @@ function spawnTimerDriver() {
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([Patch.observe(periodicTick(_$('intervalMS')))],
|
new DemandMatcher([Patch.observe(periodicTick(_$('intervalMS')))],
|
||||||
[Patch.advertise(periodicTick(_$('intervalMS')))],
|
[Patch.advertise(periodicTick(_$('intervalMS')))],
|
||||||
{
|
function (c) {
|
||||||
onDemandIncrease: function (c) {
|
Dataspace.spawn(new Tick(c.intervalMS));
|
||||||
Dataspace.spawn(new Tick(c.intervalMS));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([Patch.observe(timeLaterThan(_$('deadlineMS')))],
|
new DemandMatcher([Patch.observe(timeLaterThan(_$('deadlineMS')))],
|
||||||
[Patch.advertise(timeLaterThan(_$('deadlineMS')))],
|
[Patch.advertise(timeLaterThan(_$('deadlineMS')))],
|
||||||
{
|
function (c) {
|
||||||
onDemandIncrease: function (c) {
|
Dataspace.spawn(new Alarm(c.deadlineMS));
|
||||||
Dataspace.spawn(new Alarm(c.deadlineMS));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
js/src/ui.js
18
js/src/ui.js
|
@ -78,29 +78,23 @@ function spawnUIDriver(options) {
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([Patch.observe(globalEventProj)],
|
new DemandMatcher([Patch.observe(globalEventProj)],
|
||||||
[Patch.advertise(globalEventProj)],
|
[Patch.advertise(globalEventProj)],
|
||||||
{
|
function (c) {
|
||||||
onDemandIncrease: function (c) {
|
Dataspace.spawn(new GlobalEventSupply(c.selector, c.eventType));
|
||||||
Dataspace.spawn(new GlobalEventSupply(c.selector, c.eventType));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
var windowEventProj = windowEvent(_$('eventType'), __);
|
var windowEventProj = windowEvent(_$('eventType'), __);
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([Patch.observe(windowEventProj)],
|
new DemandMatcher([Patch.observe(windowEventProj)],
|
||||||
[Patch.advertise(windowEventProj)],
|
[Patch.advertise(windowEventProj)],
|
||||||
{
|
function (c) {
|
||||||
onDemandIncrease: function (c) {
|
Dataspace.spawn(new WindowEventSupply(c.eventType));
|
||||||
Dataspace.spawn(new WindowEventSupply(c.eventType));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Dataspace.spawn(
|
Dataspace.spawn(
|
||||||
new DemandMatcher([uiFragment(_$('fragmentId'), __, __)],
|
new DemandMatcher([uiFragment(_$('fragmentId'), __, __)],
|
||||||
[uiFragmentExists(_$('fragmentId'))],
|
[uiFragmentExists(_$('fragmentId'))],
|
||||||
{
|
function (c) {
|
||||||
onDemandIncrease: function (c) {
|
Dataspace.spawn(new UIFragment(c.fragmentId));
|
||||||
Dataspace.spawn(new UIFragment(c.fragmentId));
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Dataspace.spawn(new LocationHashTracker(options.defaultLocationHash || '/'));
|
Dataspace.spawn(new LocationHashTracker(options.defaultLocationHash || '/'));
|
||||||
|
|
Loading…
Reference in New Issue