Skip to content
Maciej Skierkowski edited this page Apr 27, 2016 · 1 revision

Factor.io Workflow Syntax

Promises

Factor.io DSL uses the JS-inspired Promises design to define the work to be done by the workflow.

Here is what you need to know about promises...

  • The run command returns a Factor::Promise.
  • Promises can be chained together in a tree structure with zero or more children using the then method.
  • The then method returns another Promise. It accepts a block that is called on successfull fullfilment of the Promise.
  • Promises can also be aggregated with methods like any and all so you can execute multiple promises in parallel and wait for the intended result. more info
  • Promises can also fail, in which case you can use rescue method with a block to capture the exception.
  • Creating a Promise merely defines the work, it does not queue it or execute it.
  • To queue the work, you must call execute on the Promise.
  • execute queues the work, but it does not wait for it to complete. It too returns a Promise.
  • To wait for a Promise to complete (i.e. block the thread), you can call wait.
  • As you can gather from the last points, a Promise can be in four states, unscheduled, pending, rejected and fulfilled, and you can get the state of a Promise at any point by calling state. You also get the convenient unscheduled?, pending?, rejected? and fulfilled? methods to check the state. There is also completed? method which checks if the Promise is either rejected or fulfilled.
  • If a Promise is fulfilled you get the value in the then block, but you can also get it by calling value.
  • Similarly, if a Promise is rejected, you get the exception in the rescue block, but you also get it by calling reason.
# Here we create the Promise request to create the Github issue
comment_post = run 'github::issues::create', repo:'skierkowski/hello', title:'', message:msg

# This above method returns immediately without doing any work.
# Now we define what we should do when the issue is created successfully.
comment_complete = comment_post.then do |comment_create_response|
  success "The issue was posted successfully"
end.rescue do |ex|
  error "Something went wrong: #{ex}"
end

# With the above we chained together two promise promises (then & rescue)
# Now lets queue the work.

executing = comment_complete.execute

# The above queues the work, but doesn't block on it. If we want to block, we an do this...

executing.wait

# In the real world you would probably do all of this in a single go...
# github_comment = run('github::issues::create',...).then{ ... }.rescue{...}
# github_comment.execute.wait

Events

Most Connectors execute commands and return the result when complete; however, some connectors are also capable of emitting events to which a workflow can subscribe. This is particularly useful for doing things like creating a chat bot or listening for a git push on a Github repo. These Connectors are implemented in a way that they never finish executing, that is, when you run wait on it's Promise, it will never complete unless you terminate it.

Subscribing to an event

You can listen to events by calling on on the promise and passing in the event name and a block to execute. The :trigger event is an event the connector will emit when the event occurs. There are also logging event types, those are cover later.

git_push = run('github::repo::push', repo:'skierkowski/hello')
git_push.on :trigger do |push_info|
  success "User #{push_info[:username]} just pushed commit #{push_info[:commit][:id]}"
end
git_push.execute.wait

Subscribing to multiple events

skierkowski_push = run 'github::repo::push', repo:'skierkowski/hello'
foo_push         = run 'github::repo::push', repo:'skierkowski/foo'
listener = on :trigger, skierkowski_push, foo_push do |push_info|
  success "User #{push_info[:username]} just pushed commit #{push_info[:commit][:id]}"
end
listener.execute.wait

In addition to subscribing to a single event, sometimes you may want to subscribe to multiple events to take the same action. For example, if you have a test run that depends on two repos, you'll want to run the tests when either one of the repos changes.

Logging from Connector

git_push = run('github::repo::push', repo:'skierkowski/hello')
git_push.on :log do |type, push_info|
  log type, push_info
end
git_push.on :error do |push_info|
  error push_info
end
git_push.execute.wait

In addition to :trigger events, Connectors can also emit logging events. There are four types of logging events, :info, :warn, :success, and :error. Each of those events will be emitted passing in the string message to the block. Additionally there is the more generic :log trigger which passes in the type and string.

Aggregating results

Sometimes you may want to execute a number of tasks at the same time, like provisioning 5 new EC2 instances, and then continue if either one, all, or n of m are completed. In these cases you can use the any or all aggregator methods.

s1 = run 'aws::ec2::create', ...
s2 = run 'aws::ec2::create', ...
s3 = run 'aws::ec2::create', ...

all_ready = all(s1, s2, s3).then |results|
  success "ALL the servers were provisioned"
end.execute.wait

at_least_one_ready = any(s1, s2, s3).then |results|
  success "At least one server is provisioned"
end.execute.wait

two_ready = any?(2, s1, s2, s3).then |results|
  success "At least two servers were provisioned"
end

In the above three cases we have different ways of aggregating. With all you can wait for all the promises to be fulfilled, with any you can wait for just one to complete, and with any with a Integer variable you can make sure that at least n were fulfilled.

The all and any methods also return Promises which is why we use then to wait for them to be complete. You can also use rescue to catch cases where the condition isn't met.

Additionally, you can also provide a block to evaluate the results.

s1 = run 'aws::ec2::create', ...
s2 = run 'aws::ec2::create', ...
s3 = run 'aws::ec2::create', ...

all_ready = all s1, s2, s3 do |result|
  result.status == :ready
end

all_ready.then |results|
  success "ALL the servers were provisioned and in :ready state"
end.execute.wait

Without the block any and all just make sure that the child Promises are fulfilled. They don't evaluate the results. By passing in the block you can evaluate the values too (e.g. status==:ready).