Interacting with the Environment

Cider-CI encourages reproducibility in many ways. But sometimes a job can depend on external resources which may change over time in unpredictable ways. This article discusses how to configure a project for Cider-CI to work with a potentially volatile environment.

  • updated: 2016-06-03
  • created: 2015-11-29
  • level: advanced

Background

A job in Cider-CI is bound to the tree_id of the repository. The tree_id is fingerprint of the contents of the repository for a given commit. The key of the job and the tree_id are combined to a unique identity in Cider-CI. This is an important feature of Cider-CI as it provides transparency, reproducibility and avoids overhead.

It can also imposes some inconvenience when working with changing external resources as it requires to change and submit the repository contents to rerun a job and in consequence forces us to rerun all related jobs.

An Example Use Case - Interacting with External APIs

Consider the usage of an external API for example. In an ideal world the external API will use dedicated versions which in turn would be specified in our project. This causes the creation of a new tree_id which links our project to the external API in a unique and traceable way. In reality the external API might not proceed in such stringent ways but change ad hoc without publishing changes. In such a case we probably might not want to create a new commit and rerun all associated jobs but just rerun a particular job that covers the interaction with the external service.

Configuring the Project to Work with External Resources

We will illustrate the project configuration by the example of a deploy job. There are many volatile properties involved. For example the state of the server, or the state of the database which undergoing migrations during the deploy.

We will focus on the two directives exclusive_global_resources and aggregate_state in the following sections.

The shown deploy configuration is a slightly simplified variant of the original which can be found in the Madek Deploy Project.

jobs:
  deploy-to-test:
    name: Deploy to "test.madek"
    context:
      tasks:
        deploy:
          exclusive_global_resources:
            "test.madek.zhdk.ch": true
          aggregate_state: satisfy-last
          traits:
            Ansible 2: yes
          scripts:
            deploy:
              body: >
                ansible-playbook
                  play_setup-and-deploy.yml

Avoiding Collisions with the exclusive_global_resources Property

The exclusive_global_resources prevents concurrent access to the same resource. The syntax based on maps is used to provide composability and is discussed in the Composing Data page of the documentation.

exclusive_global_resources:
  "test.madek.zhdk.ch": true

Running concurrent deploys to the same server would almost certainly cause problems and can even leave the server in a non recoverable state. Cider-CI will ensure that at most one trial is executing at any time with respect to the string test.madek.zhdk.ch. This spans over jobs and even projects. It is therefore important to specify the resource precisely. Using simple strings like test can easily lead to unintended locking and delays. However, deadlocks are impossible because tasks never depend on each other.

Making the Most Recent Trial Count with the aggregate-state Property

A task features the aggregate_state property. The value controls how the state of a task is aggregated.

aggregate-state: satisfy-last

The default value satisfy-any will let the corresponding task to assume the passed state if any of its trial has passed. This is the core functionality on which the resilience against false test negatives is build.

For working with changing external environments the value can be set to satisfy-last. Then the state of the task corresponds to the state of the trial created most recently.

A job where all tasks are set to satisfy-any will always stay passed if it has reached this state once. The state of a job where the aggregate_state property of some task is set to satisfy-last is volatile. It can possibly switch to any state at any time later. It can switch from passed to failed, and temporarily from a terminal state to a non terminal state.

Triggering Jobs and Retries via the API

We likely need to act on external events to make use of of the statisfy-last setting.

Cider-CI provides internal mechanisms to trigger the execution of a job via branch updates or changes of the state where the target job depends on. Jobs and retries can also be triggered via user interaction or posting actions via the API. The latter is apparently suitable for interaction from external events. We will show a typical scenario in the following. We will use the Ruby JSON-ROA Client which in turn uses the JSON-ROA extension of the Cider-CI API.

We initialize the client in the first step.

@client = JSON_ROA::Client.connect API_BASE_URL do |conn|
  conn.basic_auth(API_LOGIN, API_PASSWORD)
end

Next, we retrieve the current tree_id of the targeted branch and repository.

def get_tree_id(repository_url, name_branch_head)
  @client.get.relation(:commits) \
    .get(repository_url: repository_url,
         branch_head: name_branch_head) \
    .collection.first.get.data[:tree_id]
end

We either want execute a job if it does not exist yet, or create a new trial otherwise. This code will return an id if the job exists or nil otherwise.

def get_job_id(tree_id, key)
  job_rel = @client.get.relation(:jobs) \
            .get(tree_id: tree_id, key: key) \
            .collection.first
  job_rel && job_rel.get.data[:id]
end

Creating a new job can be performed by sending the tree_id and the key as defined in the project configuration of the job via a post request.

def create_job(tree_id, key)
  @client.get.relation(:create_job) \
    .post({}, { tree_id: tree_id, key: key }.to_json,
          content_type: 'application/json')
end

Jobs like a deploy will often have only one task. This code creates a retry for the first (and only) task.

def retry(job_id)
  @client.get.relation(:job).get(id: job_id) \
    .relation(:tasks).get.collection.first.get \
    .relation(:trials).get.relation(:retry).post
end

The examples above have been extracted from the Madek Nightly-Deploy-Script. The code given in the project provides a complete example.

Conclusion

The default project configuration in Cider-CI encourages to take the source code as fundamental truth of a project which solely determines the outcome of a job. Cider-CI provides configuration properties to additionally take volatile environments into account.