cliffhanger
Cliffhanger is a distributed graph operation language for programming microservice swarms.
Fibonacchi example
File <project_root>/fibonacchi.ct
that could handle HTTP requests to /fibonacci/<number>
paths:
-- usage:
-- GET /fibonacchi/10
WHEN name($FIELD) = 1:
= 1
ELSE:
= $(( \
$( @ ./$(( $FIELD - 1 )) ) + \
$( @ ./$(( $FIELD - 2 )) ) \
)
Acknowlegments
This language was highly influenced by the following technologies and cultural artifacts:
- JavaScript
- Forth
- SAIL programming language by Appian
- RMPL (Reactive Model-based Programming Language for robotic space explorers) by MIT
- SQL
- Apache Kafka
- Java streaming API
- the "Love is..." bubble gum.
Implementation
The language is under (yet another) redesign, so no current implementations are available.
Cliffhanger VM
Cliffhanger applications are executed using a distributed virtual machine that loads cliffhanger definitions from a file system-like data sources and executes them against a distributed data tree.
Cliffhanger modules
Cliffhanger modules are folders with '.cliff' extension in their name that contain a data tree template stored as a tree of files in which file paths and file names are directly mapped onto VM's data tree with the exception of .ct
files that define cliffhanger triggers and for which file extensions will be excluded from the path.
Examples:
- The value of file
module.cliff/example/readme
will be accessible via path/example/readme
on the data tree module.cliff/example/img/logo.png
=>/example/img/logo.png
- The value of file
module.cliff/example/feedback.ct
will be parsed as cliffhanger definitions that will be attached to path/example/feedback
Data Tree
Cliffhanger VM maintains a tree of scalar values.
Tree values can be referenced by their URLs.
The default url schema and domain refer to the Data tree and supports querying using XPath:
/user/01/username
/current/user/email
/user/*[active]
/user/*[groups/admin]
...
Note: in the future, other schemas (for example, fs:
) may support XPath as well.
Distributed Data Trees
A Data Tree becomes distributed when a peering connection is established between two or more cliffhanger nodes. In such a situation, the branches of the Data Tree may be assigned to different nodes.
Global Branch
All paths located under the /global
branch are replicated to all nodes running the Cliffhanger VM machine.
VM Information Branch
All information about the cliffhanger VM is located under the /global/vm
branch.
Node Information Branch
All information about the current Cliffhanger node is located under the /global/vm/nodes/<NODE_ID>
branch.
A reference to the current node's information branch is alvays accessible as $NODE
variable.
Any changes under a node information branch MUST be submitted to and processed and replicated by the corresponding node.
Trigger Layers
The VM loads every cliffhanger module into a separate trigger layer.
Trigger events
When a new value is written onto the Data Tree, VM will execute associated with the path of the value triggers from all loaded and active layers. Data Tree change events are propagated both horizontally through loaded layers in reverse order of layer loading and vertically through the path of the changed value from the value to the root of the Data Tree.
Data Triggers
Data triggers are the main building blocks of Cliffhanger applications. A trigger consists of an optional trigger condition and a list of mutations to be performed when the trigger is activated. Every time the sub-tree under the address to which a trigger is attached changes, the new sub-tree value is evaluated against the trigger condition. When the trigger condition evaluates to a truthy value, the trigger is activated and its mutations are executed. Inside of a layer, all triggers are activated in the direct definition order.
Value Triggers
Value triggers are defined in *.ct
files and attach directly to a value on the Data Tree.
These triggers are mapped onto the Data Tree according to the path and name (excluding the extension) of the file in which they are defined.
Trigger Conditions
Trigger conditions can be used to activate triggers depending on the state of Cliffhanger VM.
Trigger conditions are defined using when-else when-else
statements with conditions written in XPath.
Trigger conditions must start at the beginning of a new line and end with a new line character.
Immediately succeding trigger conditions are merged into a single trigger condition using logical OR
operator so, the following:
WHEN $THIS = 1:
WHEN $THIS =-1:
... list of Mutations
ELSE WHEN $THIS = 0:
... some other Mutations
ELSE:
.. fallback mutations
is equal to:
WHEN $THIS = (1, -1):
... list of Mutations
ELSE WHEN $THIS = 0:
... some other mutations
ELSE:
.. fallback mutations
Nested triger conditions are merged with parent conditions using logical AND
operator:
WHEN $THIS = 1:
WHEN $OLD = 0:
... list of Mutations #1
WHEN $OLD = -1:
... list of Mutations #2
is equal to:
WHEN $THIS = 1 and $OLD = 0:
... list of Mutations #1
WHEN $THIS = 1 and $OLD = -1:
... list of Mutations #2
Empty conditions at any level are still merged via OR
, so:
WHEN $THIS = 1:
WHEN $OLD = 0:
WHEN $OLD = -1:
... list of Mutations
is equal to:
WHEN $THIS = 1 and ($OLD = 0 or $OLD = -1):
... list of Mutations
ALWAYS shorthand
ALWAYS:
...
expands to:
WHEN true:
...
Trigger Mutations
Mutations are defined using HTTP request protocol with separation of request headers and body done via adding whitespace offset to the body lines:
WHEN $THIS = 1:
POST https://example.com/example/address
Content-Type: text/plain
echo $(@ $THIS)
Every mutation definition in a mutation list must have the same whitespace prefix. Every body line inside a mutation definition must have the same whitespace prefix.
Mutations must be separated from each other using an empty line:
WHEN $THIS = 1:
POST /data/tree/path
echo "$(@ $THIS)" | grep 'name'
POST https://example.com/example/address
Content-Type: text/plain
echo "$(@ $THIS)"
DELETE fs://some/file/path/$(@ $OLD)
PUT fs://some/file/path/$(@ $OLD)
attributes: a+r
date
Every HTTP header definition is evaluated as a BashL string using /bash -c echo <definiiton>
.
Body definitions are evaluated as bash scripts.
POST mutation short form:
POST mutations can be shorted to an assignment expression:
ALWAYS:
/path = value
equals to:
ALWAYS:
POST /path
echo "value"
Access Guards
Access Guards are executed every time a value is read from the Data Tree and defined similarly to triggers but differ from them because they define HTTP responses instead of HTTP requests:
WHEN not($REQUEST/header/Authorization):
HTTP 401 Unauthorized
X-Error-Code: no-auth-header
echo "You are not authorized to perform this operation."
echo "(Missing Authorization header)"
Access gurads can also be used to transform the response values:
ALWAYS:
HTTP 200 OK
echo "transformed value: $THIS"
200 OK shorthand
200 OK access guards can be shortened to a no-path assignment statement:
ALWAYS:
= value
expands to:
ALWAYS:
HTTP 200 OK
echo "value"
Redirects
Redirects are Access Guards defined using the REDIRECT
keyword that results in either delegating the request to another path on the same node or sending HTTP 301 Moved Permanently
response to the client.
For example,
ALWAYS:
REDIRECT /info/status
will redirect all requests to the value located at /info/status
address.
Proxies
Proxies are Access Guards defined usint the PROXY
keyword that instructs the virtual machine to serve as an HTTP or memory proxy from the guarded endpoint to the target endpoint.
Trigger Variables
The following variables are available to all triggers:
name | description |
---|---|
OLD | contains a local tree reference to the old value that will be overwritten |
THIS | contains a local tree reference to this node |
REQUEST | contains a local tree reference to the information about the originating request |
FIELD | contains the first element of the relative path from the trigger to the changed value |
NODE | contains a local tree reference to the current node branch |
Vm Peering
VM peering is the mechanism by which two cliffhanger VMs share the same Data Tree. When a peering VM joins another (master) VM, it triggers Data Tree rebalancing operation during which some of the elements of the master VM Data Tree are transferred together with attached to them triggers and guards onto the joining VM and both master and peering VM become nodes of a distributed Cliffhanger VM.
When a node of a distributed VM is requested to perform an operation on a part of the distributed Data Tree that is assigned to another node, it will redirect the request to the appropriate node using HTTP 301 Moved Permanently
response.
To do so, all nodes maintain a list of mappings that is available in the Data Tree under the /global/vm/mappings
path prefix.
Tree rebalancing
Tree rebalancing is performed by participating nodes transparently by reassigning tree branches based on their computational complexity. To do this, every node calculates computational complexity for each mapped to it tree path based on:
- percentage of memory occupied by the corresponding tree branch
- percentage of CPU time spent on serving the corresponding tree branch
Node's available computational power is also calculated and periodically updated by timing a sample computational task.
This available computational power is than stored as /global/vm/nodes/<NODE_ID>/power
value.
This information is then used to normalize calculated computational complexity across the cluster level.
Normalized calculational complexities are then published by the node by updating the /global/vm/mappings/<MAPPING_ID>/complexity
value.
Each peering node can be configured to maintain a specific computational power usage levels. When a node detects that it uses too much or too little of computational power, it attempts to either shed some of its mappings or gain more of them.
Tree mappings
Tree mappings are used by participating nodes to determine which nodes serve which part of the Data Tree.
All changes to a tree mapping must either be initiated by the mapped node or submitted onto it and replicated by the mapped node to all other nodes.
When resolving tree paths with mappings, the most specific mapping should be selected by the requesting node: a mapping of /data/users/01
should be picked over the mapping of /data
.
Mapping shedding
To shed a mapping, a node first decides which specific mapping needs to be shed:
- If only single mapping is maintained by the node, then it attempts to split it into two mappings. The node attempts to size one of the mappings according to the amount of computational complexity it wants to shed and then selects that mapping to be shed.
- When more than a single mapping is maintained by the node, the node sorts mappings by their complexity and then picks the first mapping that is bigger than desired complexity amount.
The picked mapping is then marked by writing "SHEDDING" into /global/vm/mappings/<MAPPING_ID>/status
value.
Mapping gaining
To gain a mapping, the node selects all mappings in the "SHEDDING" status and then selects the last mapping that is smaller than the desired complexity gain.
Then it sends an HTTP POST request to the node that wants to shed the mapping that updates the /global/vm/mappings/<MAPPING_ID>
path with the following values:
status: MOVING
node: <GAINING_NODE_ID>
The request to move the mapping MUST also contain a transaction ID in the X-Transaction
header.
The gaining node then replicates this request to all participating nodes except the gaining node and responds with HTTP 202 Accepted
when replication succeds.
When a mapping is in the MOVING
state, all requests to the corresponding tree branch by participating nodes MUST be deffered until the mapping changes its status to ACTIVE
.
During this period the shedding and gaining nodes MUST respond with yaml-encoded mapping value and 503 Service Unavailable
responses to any requests to the associated with the mapping tree branch.
When the shedding node is ready to perform the transfer, it sends a multi-part HTTP POST request with the same X-Transaction
header as in the originating transfer request.
The response body should contain yaml-encoded value of the tree branch and its trigger and guard definitions as its parts.
Upon successfuly receiving the branch value, gaining node responds with HTTP 202 Accepted
status.
When the gaining node is ready to start serving the branch, it updates the /global/vm/mappings/<MAPPING_ID>/status
value to ACTIVE
and replicates this change to all other participating nodes by sending HTTP POST requests to every node.
Mapping Locking
System administrators may create locked mappings by setting /global/vm/mappings/<MAPPING_ID>/locked
path to a truthy value.
Locked mappings cannot be rebalanced from the node onto which they map.
For example, the path /current/user/phone/screen
could be locked onto the node that represents user's phone, etc.
Internal Communication
TODO: describe node properties that store information about supported by the node communication methods.
IDEA: use node-generated triggers for communication?
External Communication
Sending Data to a Cliffhanger VM
A cliffhanger VM accepts external data changes as HTTP POST requests submitted to the VM Data Api port.
Request paths are mapped directly onto the Data Tree.
Bulk value updates can be prformed by submitting requests with Content-type
of either application/json
or application/yaml
.
Requesting Data from a Cliffhanger VM
VM Data Tree values can be requested using HTTP GET requests submitted to the VM Data Api port. All data responses are yaml-formatted.
Pushing data out of a Cliffhanger VM
The nature of the language allows to push data to external systems directly from trigger mutations:
ALWAYS:
POST https://some-other-system.com/some/path
Authentication: Bearer "$(@ /external/some-other-system/auth)"
echo "$THIS"
Subscribing to changes
An external client or peering VM may subscribe to all changes under a data tree branch by opening a websocket on corresponding to the branch path on the node to which that path belongs. If such a request is sent to any different node, it MUST respond with a temporary redirect to the correct node.
Related Projects and ideas
JavaScript client library
It would be nice to have a library that can be used to interact with Cliffhanger data tree from the browser.
WebGL renderer
It would be nice to have a component that allows rendering cliffhanger data trees on an html web page.
VR editor
It would be nice to implement an editor app that would allow working on cliffhanger data trees using a browser in a standalone VR headset.
Cliffhanger nginx manager
It would be nice to have a service that would manage local nginx configuration to provide node-targeted paths for external clients that can't communicate with the nodes dorectly.