fanfilmu/conflow

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Conflow

[![Gem Version](https://badge.fury.io/rb/conflow.svg)](https://badge.fury.io/rb/conflow) [![Build Status](https://travis-ci.org/fanfilmu/conflow.svg?branch=master)](https://travis-ci.org/fanfilmu/conflow) [![Maintainability](https://api.codeclimate.com/v1/badges/80b66a285ca1803f391a/maintainability)](https://codeclimate.com/github/fanfilmu/conflow/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/80b66a285ca1803f391a/test_coverage)](https://codeclimate.com/github/fanfilmu/conflow/test_coverage)

Conflow allows defining complicated workflows with dependencies. Inspired by [Gush](https://github.com/chaps-io/gush) (the idea) and [Redis::Objects](https://github.com/nateware/redis-objects) (the implementation) it focuses solely on dependency logic, while leaving queueing jobs and executing them entirely in hands of the programmer.

Please have a look at `Gush` if you already use Rails and ActiveJob - it might suit your needs better.

## Installation

Add this line to your application's Gemfile:

```ruby
gem "conflow"
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install conflow

## Usage

### Configuration

#### Redis connection

To configure Redis connection, set `Conflow.redis` attribute to a `Redis` or `ConnectionPool` instance.

```ruby
Conflow.redis = Redis.new(host: "127.0.0.1", port: 6379)
# or
require "connection_pool"
Conflow.redis = ConnectionPool.new(size: 5, timeout: 5) { Redis.new(host: "127.0.0.1", port: 6379) }
```

#### Redis script caching

By default, gem caches it's scripts in Redis server. To disable this behaviour, set `cache_scripts` to false:

```ruby
Conflow::Redis::Scripts.cache_scripts = false
```

### Defining flows

In order to define a flow, first you need to supply a way to enqueue jobs.

`Conflow` does not make any assumptions about this process - you can enqueue Sidekiq job, send a RabbitMQ event or send an email to a Very Important Person with flow ID and job ID.

```ruby
class ApplicationFlow < Conflow::Flow
  def queue(job)
    Sidekiq::Client.enqueue(FlowWorkerJob, id, job.id)
  end
end
```

`id` (`Conflow::Flow#id`) and `job.id` (`Conflow::Job#id`) is enough to identify job and execute it properly. Make sure that you send both of these values and it will be OK.

You can define actual jobs to be performed using `#configure` method:

```ruby
class MyFlow < ApplicationFlow
  def configure(id:, strict:)
    run UpsertJob, params: { id: id }
    run CheckerJob, params: { id: id }, after: UpsertJob if strict
  end
end
```

To create flow, use `.create` method:

```ruby
MyFlow.create(id: 320, strict: false)
MyFlow.create(id: 15, strict: true)
```

#### Dependencies

You can use `after` option to define dependencies. `after` accepts a `Class`, `Conflow::Job` instance or `Integer` with id of the job - or an array with any combination of these.

```ruby
class MyFlow < ApplicationFlow
  def configure
    first = run FirstJob
    independent = run IndependentJob

    run SecondJob, after: [FirstJob, independent]
    run FinishUp, after: SecondJob
  end
end
```

![Created graph](https://camo.githubusercontent.com/0b1ee59994323900906264ea50fbc9169e4d21dd/68747470733a2f2f63686172742e676f6f676c65617069732e636f6d2f63686172743f63686c3d646967726170682b472b2537422530442530412b2b72616e6b6469722533444c522533422530442530412b2b25323253544152542532322b2d2533452b25323246697273744a6f622532322530442530412b2b25323253544152542532322b2d2533452b253232496e646570656e64656e744a6f622532322530442530412b2b25323246697273744a6f622532322b2d2533452b2532325365636f6e644a6f622532322530442530412b2b253232496e646570656e64656e744a6f622532322b2d2533452b2532325365636f6e644a6f622532322530442530412b2b2532325365636f6e644a6f622532322b2d2533452b25323246696e69736855702532322530442530412b2b25323246696e69736855702532322b2d2533452b253232454e442532322530442530412537442530442530412b266368743d6776)

#### Promises

In order to use other Job's result as parameter of another job, use Futures:

```ruby
class MyFlow < ApplicationFlow
  def configure
    first = run FirstJob
    run SecondJob, params: { object_id: first.outcome[:id] }
  end
end
```

Note that `SecondJob` will automatically depend on `FirstJob`. When `FirstJob` finishes, it is expected to return hash: `{ id: "<some object>" }`.

Returned object must be serializable with JSON in order to be properly persisted and handled by Redis script which resolves promises.

If `FirstJob` returns `{ id: 14 }`, `SecondJob` will be run with `{ object_id: 14 }` parameter.

### Performing jobs

To perform job, use `Conflow::Worker` mixin. It adds `#perform` method, which accepts two arguments: IDs of the flow and the job.

Simple `Conflow::Worker` that is also `Sidekiq::Worker`:

```ruby
class FlowWorkerJob
  include Conflow::Worker
  include Sidekiq::Worker # order is important!

  def perform(flow_id, job_id)
    super do |worker_class, params|
      worker_class.new(params).call
    end
  end
end
```

For previously defined flow, executing this flow would result in:

```ruby
FirstJob.new({}).call
IndependentJob.new({}).call # order of the first two is not defined

SecondJob.new({}).call

FinishUp.new({}).call
```

## Theory

The main idea of the gem is, obviously, a directed graph containing information about dependencies. It is stored in Redis in following fields:

* `conflow:job:<id>:successors` - ([List](https://redis.io/topics/data-types#lists)) containing IDs of jobs which depend on `<id>`
* `conflow:flow:<id>:indegee` - ([Sorted Set](https://redis.io/topics/data-types#sorted-sets)) set of all unqueued jobs with score representing how many dependencies are not yet fulfilled

There are three main actions that can be performed on this graph (Redis-wise):

1. Queue jobs
   Removes all jobs with score 0 from `:indegree` set
2. Complete job
   Decrement scores of all of the job's successors by one
3. Add job
   Add job ID to `:successors` list for all jobs on which it depends and add job itself to `:indegree` set

All of these actions are performed via `eval`/`evalsha` - it lifts problems with synchronization (as scripts are executed as if in transaction) and significantly reduces amount of requests made to Redis.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/conflow. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Code of Conduct

Everyone interacting in the Conflow project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/conflow/blob/master/CODE_OF_CONDUCT.md).