gush 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12fdb9a62d33353f827194c198c011ff42d7491669d0ab9717be0920a8066313
4
- data.tar.gz: 7f3d4ada23215e818f0cb801c92f4752e80b21b344f3b48d9ab4b83787c029ec
3
+ metadata.gz: dc70498c39ad506749bed1f55c139d29691e540b849dd39e2ba132b54b511d66
4
+ data.tar.gz: 700d093574e0ff4772c7d36bfeb7792cda44550dd162a48afe17bec5551250cd
5
5
  SHA512:
6
- metadata.gz: bec4bcb3e251bdb1a2e184b6b62ae896fd40af4a0cd4bad2ec7ee4925ab356e36a3388d2cb78ed21582c0f6cfdd429c304f0591a5ee3760c73255d03a2b1d80e
7
- data.tar.gz: 67fba0b65b575449114233a4ff9cfaf1cb6ac3ff61633f94b7c595988165b3315fe7ae5d0744792315d9329795da0e42ae840789e3d4f72c84ef8956081c5fbd
6
+ metadata.gz: ac4c1647f57a87600466445a8d24660e3d95016f17adf9e2fe8f99260ba82a2b3e22a339b9456f653834c89c6e4b9ed6bcb50ebc337b804e07355f9a513ce8ca
7
+ data.tar.gz: 1ae01511acfc2e23bb0a671328a7b3e9b650b8589b9620461c2b9667ff9d80a47f03b8a156f249746b4f69e613863db0833be8d6221acdabd211d7eba2daee15
@@ -0,0 +1,71 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ pull_request:
12
+ paths-ignore:
13
+ - 'README.md'
14
+ push:
15
+ paths-ignore:
16
+ - 'README.md'
17
+
18
+ jobs:
19
+ test:
20
+ services:
21
+ redis:
22
+ image: redis:alpine
23
+ ports: ["6379:6379"]
24
+ options: --entrypoint redis-server
25
+
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ matrix:
29
+ rails_version: ['4.2.7', '5.1.0', '5.2.0', '6.0.0', '6.1.0', '7.0']
30
+ ruby-version: ['2.6', '2.7', '3.0', '3.1']
31
+ exclude:
32
+ - ruby-version: '3.0'
33
+ rails_version: '4.2.7'
34
+ - ruby-version: '3.1'
35
+ rails_version: '4.2.7'
36
+ - ruby-version: '3.0'
37
+ rails_version: '5.0'
38
+ - ruby-version: '3.1'
39
+ rails_version: '5.0'
40
+ - ruby-version: '3.0'
41
+ rails_version: '5.1'
42
+ - ruby-version: '3.1'
43
+ rails_version: '5.1'
44
+ - ruby-version: '3.0'
45
+ rails_version: '5.2'
46
+ - ruby-version: '3.1'
47
+ rails_version: '5.2'
48
+ - ruby-version: '3.0'
49
+ rails_version: '6.0'
50
+ - ruby-version: '3.1'
51
+ rails_version: '6.0'
52
+ - ruby-version: '3.1'
53
+ rails_version: '6.1'
54
+ - ruby-version: '2.6'
55
+ rails_version: '7.0'
56
+ steps:
57
+ - uses: actions/checkout@v2
58
+ - name: Set up Ruby
59
+ uses: ruby/setup-ruby@v1
60
+ with:
61
+ ruby-version: ${{ matrix.ruby-version }}
62
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
63
+ env:
64
+ RAILS_VERSION: "${{ matrix.rails_version }}"
65
+ - name: Install Graphviz
66
+ run: sudo apt-get install graphviz
67
+ - name: Run tests
68
+ run: bundle exec rspec
69
+ env:
70
+ REDIS_URL: redis://localhost:6379/1
71
+ RAILS_VERSION: "${{ matrix.rails_version }}"
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 2.1.0
9
+
10
+ ### Added
11
+
12
+ - Allow RedisMutex’s locking duration and polling interval to be customizable, thanks to @thukim! [See pull request](https://github.com/chaps-io/gush/pull/74)
13
+ - Support for Rails 7.0 and Ruby 3.0-3.1, thanks to @joshRpowell and @kzkn!
14
+
8
15
  ## 2.0.1
9
16
 
10
17
  ### Fixed
@@ -13,31 +20,31 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
13
20
 
14
21
  ## 2.0.0
15
22
 
16
- ## Changed
23
+ ### Changed
17
24
 
18
25
  - *[BREAKING]* Store gush jobs on redis hash instead of plain keys - this improves performance when retrieving keys (Thanks to @Saicheg! [See pull request](https://github.com/chaps-io/gush/pull/56))
19
26
 
20
27
 
21
- ## Added
28
+ ### Added
22
29
 
23
30
  - Allow setting queue for each job via `:queue` option in `run` method (Thanks to @devilankur18! [See pull request](https://github.com/chaps-io/gush/pull/58))
24
31
 
25
32
 
26
33
  ## 1.1.1 - 2018-06-09
27
34
 
28
- ## Changed
35
+ ### Changed
29
36
 
30
37
  - Relax dependency on ActiveSupport to work with 4.2 up to 5.X (Thanks to @iacobus! [See pull request](https://github.com/chaps-io/gush/pull/54))
31
38
 
32
39
 
33
40
  ## 1.1.0 - 2018-02-05
34
41
 
35
- ## Added
42
+ ### Added
36
43
 
37
44
  - Added ability to specify TTL for Redis keys and manually expire whole workflows (Thanks to @dmitrypol! [See pull request](https://github.com/chaps-io/gush/pull/48))
38
45
  - Loosened dependency on redis-rb library to >= 3.2 and < 5.0 (Thanks to @mofumofu3n! [See pull request](https://github.com/chaps-io/gush/pull/52))
39
46
 
40
- ## Fixed
47
+ ### Fixed
41
48
 
42
49
  - Improved performance of (de)serializing workflows by not storing job array inside workflow JSON and other smaller improvements ([See pull request](https://github.com/chaps-io/gush/pull/53))
43
50
 
data/Gemfile CHANGED
@@ -1,6 +1,10 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
+ rails_version = ENV['RAILS_VERSION'] || '< 7.0'
5
+ rails_version = "~> #{rails_version}" if rails_version =~ /^\d/
6
+ gem 'activejob', rails_version
7
+
4
8
  platforms :mri, :ruby do
5
9
  gem 'yajl-ruby'
6
10
  end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Gush [![Build Status](https://travis-ci.org/chaps-io/gush.svg?branch=master)](https://travis-ci.org/chaps-io/gush)
1
+ # Gush
2
2
 
3
3
  Gush is a parallel workflow runner using only Redis as storage and [ActiveJob](http://guides.rubyonrails.org/v4.2/active_job_basics.html#introduction) for scheduling and executing jobs.
4
4
 
@@ -8,14 +8,14 @@ Gush relies on directed acyclic graphs to store dependencies, see [Parallelizing
8
8
 
9
9
  ## **WARNING - version notice**
10
10
 
11
- This README is about the `1.0.0` version, which has breaking changes compared to < 1.0.0 versions. [See here for 0.4.1 documentation](https://github.com/chaps-io/gush/blob/349c5aff0332fd14b1cb517115c26d415aa24841/README.md).
11
+ This README is about the latest `master` code, which might differ from what is released on RubyGems. See tags to browse previous READMEs.
12
12
 
13
13
  ## Installation
14
14
 
15
15
  ### 1. Add `gush` to Gemfile
16
16
 
17
17
  ```ruby
18
- gem 'gush', '~> 1.0.0'
18
+ gem 'gush', '~> 2.0'
19
19
  ```
20
20
 
21
21
  ### 2. Create `Gushfile`
@@ -274,7 +274,7 @@ class EncodeVideo < Gush::Job
274
274
  end
275
275
  ```
276
276
 
277
- `payloads` is an array containing outputs from all ancestor jobs. So for our `EncodeVide` job from above, the array will look like:
277
+ `payloads` is an array containing outputs from all ancestor jobs. So for our `EncodeVideo` job from above, the array will look like:
278
278
 
279
279
 
280
280
  ```ruby
@@ -321,6 +321,23 @@ it will generate a workflow with 5 `NotificationJob`s and one `AdminNotification
321
321
 
322
322
  ![DynamicWorkflow](https://i.imgur.com/HOI3fjc.png)
323
323
 
324
+ ### Dynamic queue for jobs
325
+
326
+ There might be a case you want to configure different jobs in the workflow using different queues. Based on the above the example, we want to config `AdminNotificationJob` to use queue `admin` and `NotificationJob` use queue `user`.
327
+
328
+ ```ruby
329
+
330
+ class NotifyWorkflow < Gush::Workflow
331
+ def configure(user_ids)
332
+ notification_jobs = user_ids.map do |user_id|
333
+ run NotificationJob, params: {user_id: user_id}, queue: 'user'
334
+ end
335
+
336
+ run AdminNotificationJob, after: notification_jobs, queue: 'admin'
337
+ end
338
+ end
339
+ ```
340
+
324
341
  ## Command line interface (CLI)
325
342
 
326
343
  ### Checking status
@@ -346,9 +363,23 @@ This requires that you have imagemagick installed on your computer:
346
363
  bundle exec gush viz <NameOfTheWorkflow>
347
364
  ```
348
365
 
366
+ ### Customizing locking options
367
+
368
+ In order to prevent getting the RedisMutex::LockError error when having a large number of jobs, you can customize these 2 fields `locking_duration` and `polling_interval` as below
369
+
370
+ ```ruby
371
+ # config/initializers/gush.rb
372
+ Gush.configure do |config|
373
+ config.redis_url = "redis://localhost:6379"
374
+ config.concurrency = 5
375
+ config.locking_duration = 2 # how long you want to wait for the lock to be released, in seconds
376
+ config.polling_interval = 0.3 # how long the polling interval should be, in seconds
377
+ end
378
+ ```
379
+
349
380
  ### Cleaning up afterwards
350
381
 
351
- Running `NotifyWorkflow.create` inserts multiple keys into Redis every time it is ran. This data might be useful for analysis but at a certain point it can be purged via Redis TTL. By default gush and Redis will keep keys forever. To configure expiration you need to 2 things. Create initializer (specify config.ttl in seconds, be different per environment).
382
+ Running `NotifyWorkflow.create` inserts multiple keys into Redis every time it is ran. This data might be useful for analysis but at a certain point it can be purged via Redis TTL. By default gush and Redis will keep keys forever. To configure expiration you need to 2 things. Create initializer (specify config.ttl in seconds, be different per environment).
352
383
 
353
384
  ```ruby
354
385
  # config/initializers/gush.rb
@@ -359,7 +390,7 @@ Gush.configure do |config|
359
390
  end
360
391
  ```
361
392
 
362
- And you need to call `flow.expire!` (optionally passing custom TTL value overriding `config.ttl`). This gives you control whether to expire data for specific workflow. Best NOT to set TTL to be too short (like minutes) but about a week in length. And you can run `Client.expire_workflow` and `Client.expire_job` passing appropriate IDs and TTL (pass -1 to NOT expire) values.
393
+ And you need to call `flow.expire!` (optionally passing custom TTL value overriding `config.ttl`). This gives you control whether to expire data for specific workflow. Best NOT to set TTL to be too short (like minutes) but about a week in length. And you can run `Client.expire_workflow` and `Client.expire_job` passing appropriate IDs and TTL (pass -1 to NOT expire) values.
363
394
 
364
395
  ### Avoid overlapping workflows
365
396
 
data/bin/gush CHANGED
@@ -12,7 +12,7 @@ require 'gush'
12
12
  begin
13
13
  Gush::CLI.start(ARGV)
14
14
  rescue Gush::WorkflowNotFound
15
- puts "Workflow not found".red
15
+ puts Paint["Workflow not found", :red]
16
16
  rescue Gush::DependencyLevelTooDeep
17
- puts "Dependency level too deep. Perhaps you have a dependency cycle?".red
17
+ puts Paint["Dependency level too deep. Perhaps you have a dependency cycle?", :red]
18
18
  end
data/gush.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "gush"
7
- spec.version = "2.0.2"
7
+ spec.version = "2.1.0"
8
8
  spec.authors = ["Piotrek Okoński"]
9
9
  spec.email = ["piotrek@okonski.org"]
10
10
  spec.summary = "Fast and distributed workflow runner based on ActiveJob and Redis"
@@ -17,20 +17,19 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "activejob", ">= 4.2.7", "< 7.0"
20
+ spec.add_dependency "activejob", ">= 4.2.7", "< 7.1"
21
21
  spec.add_dependency "concurrent-ruby", "~> 1.0"
22
22
  spec.add_dependency "multi_json", "~> 1.11"
23
23
  spec.add_dependency "redis", ">= 3.2", "< 5"
24
24
  spec.add_dependency "redis-mutex", "~> 4.0.1"
25
25
  spec.add_dependency "hiredis", "~> 0.6"
26
- spec.add_dependency "ruby-graphviz", "~> 1.2"
27
- spec.add_dependency "terminal-table", "~> 1.4"
28
- spec.add_dependency "colorize", "~> 0.7"
29
- spec.add_dependency "thor", "~> 0.19"
26
+ spec.add_dependency "graphviz", "~> 1.2"
27
+ spec.add_dependency "terminal-table", ">= 1.4", "< 3.1"
28
+ spec.add_dependency "paint", "~> 2.2"
29
+ spec.add_dependency "thor", ">= 0.19", "< 1.3"
30
30
  spec.add_dependency "launchy", "~> 2.4"
31
31
  spec.add_development_dependency "bundler"
32
32
  spec.add_development_dependency "rake", "~> 10.4"
33
33
  spec.add_development_dependency "rspec", '~> 3.0'
34
34
  spec.add_development_dependency "pry", '~> 0.10'
35
- spec.add_development_dependency 'fakeredis', '~> 0.5'
36
35
  end
@@ -17,11 +17,11 @@ module Gush
17
17
  elsif workflow.running?
18
18
  running_status
19
19
  elsif workflow.finished?
20
- "done".green
20
+ Paint["done", :green]
21
21
  elsif workflow.stopped?
22
- "stopped".red
22
+ Paint["stopped", :red]
23
23
  else
24
- "ready to start".blue
24
+ Paint["ready to start", :blue]
25
25
  end
26
26
  end
27
27
 
@@ -48,10 +48,10 @@ module Gush
48
48
  "ID" => workflow.id,
49
49
  "Name" => workflow.class.to_s,
50
50
  "Jobs" => workflow.jobs.count,
51
- "Failed jobs" => failed_jobs_count.red,
52
- "Succeeded jobs" => succeeded_jobs_count.green,
53
- "Enqueued jobs" => enqueued_jobs_count.yellow,
54
- "Running jobs" => running_jobs_count.blue,
51
+ "Failed jobs" => Paint[failed_jobs_count, :red],
52
+ "Succeeded jobs" => Paint[succeeded_jobs_count, :green],
53
+ "Enqueued jobs" => Paint[enqueued_jobs_count, :yellow],
54
+ "Running jobs" => Paint[running_jobs_count, :blue],
55
55
  "Remaining jobs" => remaining_jobs_count,
56
56
  "Started at" => started_at,
57
57
  "Status" => status
@@ -60,7 +60,7 @@ module Gush
60
60
 
61
61
  def running_status
62
62
  finished = succeeded_jobs_count.to_i
63
- status = "running".yellow
63
+ status = Paint["running", :yellow]
64
64
  status += "\n#{finished}/#{total_jobs_count} [#{(finished*100)/total_jobs_count}%]"
65
65
  end
66
66
 
@@ -69,7 +69,7 @@ module Gush
69
69
  end
70
70
 
71
71
  def failed_status
72
- status = "failed".light_red
72
+ status = Paint["failed", :red]
73
73
  status += "\n#{failed_job} failed"
74
74
  end
75
75
 
@@ -77,13 +77,13 @@ module Gush
77
77
  name = job.name
78
78
  case
79
79
  when job.failed?
80
- "[✗] #{name.red} \n"
80
+ "[✗] #{Paint[name, :red]} \n"
81
81
  when job.finished?
82
- "[✓] #{name.green} \n"
82
+ "[✓] #{Paint[name, :green]} \n"
83
83
  when job.enqueued?
84
- "[•] #{name.yellow} \n"
84
+ "[•] #{Paint[name, :yellow]} \n"
85
85
  when job.running?
86
- "[•] #{name.blue} \n"
86
+ "[•] #{Paint[name, :blue]} \n"
87
87
  else
88
88
  "[ ] #{name} \n"
89
89
  end
data/lib/gush/cli.rb CHANGED
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'terminal-table'
2
- require 'colorize'
4
+ require 'paint'
3
5
  require 'thor'
4
6
  require 'launchy'
5
7
 
@@ -12,43 +14,45 @@ module Gush
12
14
  def initialize(*)
13
15
  super
14
16
  Gush.configure do |config|
15
- config.gushfile = options.fetch("gushfile", config.gushfile)
16
- config.concurrency = options.fetch("concurrency", config.concurrency)
17
- config.redis_url = options.fetch("redis", config.redis_url)
18
- config.namespace = options.fetch("namespace", config.namespace)
19
- config.ttl = options.fetch("ttl", config.ttl)
17
+ config.gushfile = options.fetch("gushfile", config.gushfile)
18
+ config.concurrency = options.fetch("concurrency", config.concurrency)
19
+ config.redis_url = options.fetch("redis", config.redis_url)
20
+ config.namespace = options.fetch("namespace", config.namespace)
21
+ config.ttl = options.fetch("ttl", config.ttl)
22
+ config.locking_duration = options.fetch("locking_duration", config.locking_duration)
23
+ config.polling_interval = options.fetch("polling_interval", config.polling_interval)
20
24
  end
21
25
  load_gushfile
22
26
  end
23
27
 
24
- desc "create [WorkflowClass]", "Registers new workflow"
28
+ desc "create WORKFLOW_CLASS", "Registers new workflow"
25
29
  def create(name)
26
30
  workflow = client.create_workflow(name)
27
31
  puts "Workflow created with id: #{workflow.id}"
28
32
  puts "Start it with command: gush start #{workflow.id}"
29
33
  end
30
34
 
31
- desc "start [workflow_id]", "Starts Workflow with given ID"
35
+ desc "start WORKFLOW_ID [ARG ...]", "Starts Workflow with given ID"
32
36
  def start(*args)
33
37
  id = args.shift
34
38
  workflow = client.find_workflow(id)
35
39
  client.start_workflow(workflow, args)
36
40
  end
37
41
 
38
- desc "create_and_start [WorkflowClass]", "Create and instantly start the new workflow"
42
+ desc "create_and_start WORKFLOW_CLASS [ARG ...]", "Create and instantly start the new workflow"
39
43
  def create_and_start(name, *args)
40
44
  workflow = client.create_workflow(name)
41
45
  client.start_workflow(workflow.id, args)
42
46
  puts "Created and started workflow with id: #{workflow.id}"
43
47
  end
44
48
 
45
- desc "stop [workflow_id]", "Stops Workflow with given ID"
49
+ desc "stop WORKFLOW_ID", "Stops Workflow with given ID"
46
50
  def stop(*args)
47
51
  id = args.shift
48
52
  client.stop_workflow(id)
49
53
  end
50
54
 
51
- desc "show [workflow_id]", "Shows details about workflow with given ID"
55
+ desc "show WORKFLOW_ID", "Shows details about workflow with given ID"
52
56
  option :skip_overview, type: :boolean
53
57
  option :skip_jobs, type: :boolean
54
58
  option :jobs, default: :all
@@ -60,7 +64,7 @@ module Gush
60
64
  display_jobs_list_for(workflow, options[:jobs]) unless options[:skip_jobs]
61
65
  end
62
66
 
63
- desc "rm [workflow_id]", "Delete workflow with given ID"
67
+ desc "rm WORKFLOW_ID", "Delete workflow with given ID"
64
68
  def rm(workflow_id)
65
69
  workflow = client.find_workflow(workflow_id)
66
70
  client.destroy_workflow(workflow)
@@ -81,13 +85,39 @@ module Gush
81
85
  puts Terminal::Table.new(headings: headers, rows: rows)
82
86
  end
83
87
 
84
- desc "viz [WorkflowClass]", "Displays graph, visualising job dependencies"
85
- def viz(name)
88
+ desc "viz {WORKFLOW_CLASS|WORKFLOW_ID}", "Displays graph, visualising job dependencies"
89
+ option :filename, type: :string, default: nil
90
+ option :open, type: :boolean, default: nil
91
+ def viz(class_or_id)
86
92
  client
87
- workflow = name.constantize.new
88
- graph = Graph.new(workflow)
93
+
94
+ begin
95
+ workflow = client.find_workflow(class_or_id)
96
+ rescue WorkflowNotFound
97
+ workflow = nil
98
+ end
99
+
100
+ unless workflow
101
+ begin
102
+ workflow = class_or_id.constantize.new
103
+ rescue NameError => e
104
+ STDERR.puts Paint["'#{class_or_id}' is not a valid workflow class or id", :red]
105
+ exit 1
106
+ end
107
+ end
108
+
109
+ opts = {}
110
+
111
+ if options[:filename]
112
+ opts[:filename], opts[:path] = File.split(options[:filename])
113
+ end
114
+
115
+ graph = Graph.new(workflow, **opts)
89
116
  graph.viz
90
- Launchy.open graph.path
117
+
118
+ if (options[:open].nil? && !options[:filename]) || options[:open]
119
+ Launchy.open Pathname.new(graph.path).realpath.to_s
120
+ end
91
121
  end
92
122
 
93
123
  private
@@ -118,13 +148,14 @@ module Gush
118
148
 
119
149
  def load_gushfile
120
150
  file = client.configuration.gushfile
121
- if !gushfile.exist?
122
- raise Thor::Error, "#{file} not found, please add it to your project".colorize(:red)
151
+
152
+ unless gushfile.exist?
153
+ raise Thor::Error, Paint["#{file} not found, please add it to your project", :red]
123
154
  end
124
155
 
125
156
  load file.to_s
126
157
  rescue LoadError
127
- raise Thor::Error, "failed to require #{file}".colorize(:red)
158
+ raise Thor::Error, Paint["failed to require #{file}", :red]
128
159
  end
129
160
  end
130
161
  end
data/lib/gush/client.rb CHANGED
@@ -72,7 +72,7 @@ module Gush
72
72
  id = nil
73
73
  loop do
74
74
  id = SecureRandom.uuid
75
- available = !redis.exists("gush.workflow.#{id}")
75
+ available = !redis.exists?("gush.workflow.#{id}")
76
76
 
77
77
  break if available
78
78
  end
@@ -1,17 +1,19 @@
1
1
  module Gush
2
2
  class Configuration
3
- attr_accessor :concurrency, :namespace, :redis_url, :ttl
3
+ attr_accessor :concurrency, :namespace, :redis_url, :ttl, :locking_duration, :polling_interval
4
4
 
5
5
  def self.from_json(json)
6
6
  new(Gush::JSON.decode(json, symbolize_keys: true))
7
7
  end
8
8
 
9
9
  def initialize(hash = {})
10
- self.concurrency = hash.fetch(:concurrency, 5)
11
- self.namespace = hash.fetch(:namespace, 'gush')
12
- self.redis_url = hash.fetch(:redis_url, 'redis://localhost:6379')
13
- self.gushfile = hash.fetch(:gushfile, 'Gushfile')
14
- self.ttl = hash.fetch(:ttl, -1)
10
+ self.concurrency = hash.fetch(:concurrency, 5)
11
+ self.namespace = hash.fetch(:namespace, 'gush')
12
+ self.redis_url = hash.fetch(:redis_url, 'redis://localhost:6379')
13
+ self.gushfile = hash.fetch(:gushfile, 'Gushfile')
14
+ self.ttl = hash.fetch(:ttl, -1)
15
+ self.locking_duration = hash.fetch(:locking_duration, 2) # how long you want to wait for the lock to be released, in seconds
16
+ self.polling_interval = hash.fetch(:polling_internal, 0.3) # how long the polling interval should be, in seconds
15
17
  end
16
18
 
17
19
  def gushfile=(path)
@@ -24,10 +26,12 @@ module Gush
24
26
 
25
27
  def to_hash
26
28
  {
27
- concurrency: concurrency,
28
- namespace: namespace,
29
- redis_url: redis_url,
30
- ttl: ttl
29
+ concurrency: concurrency,
30
+ namespace: namespace,
31
+ redis_url: redis_url,
32
+ ttl: ttl,
33
+ locking_duration: locking_duration,
34
+ polling_interval: polling_interval
31
35
  }
32
36
  end
33
37
 
data/lib/gush/graph.rb CHANGED
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+
1
5
  module Gush
2
6
  class Graph
3
- attr_reader :workflow, :filename, :path, :start, :end_node
7
+ attr_reader :workflow, :filename, :path, :start_node, :end_node
4
8
 
5
9
  def initialize(workflow, options = {})
6
10
  @workflow = workflow
@@ -9,19 +13,26 @@ module Gush
9
13
  end
10
14
 
11
15
  def viz
12
- GraphViz.new(:G, graph_options) do |graph|
13
- set_node_options!(graph)
14
- set_edge_options!(graph)
16
+ @graph = Graphviz::Graph.new(**graph_options)
17
+ @start_node = add_node('start', shape: 'diamond', fillcolor: '#CFF09E')
18
+ @end_node = add_node('end', shape: 'diamond', fillcolor: '#F56991')
15
19
 
16
- @start = graph.start(shape: 'diamond', fillcolor: '#CFF09E')
17
- @end_node = graph.end(shape: 'diamond', fillcolor: '#F56991')
18
-
19
- workflow.jobs.each do |job|
20
- add_job(graph, job)
21
- end
20
+ # First, create nodes for all jobs
21
+ @job_name_to_node_map = {}
22
+ workflow.jobs.each do |job|
23
+ add_job_node(job)
24
+ end
22
25
 
23
- graph.output(png: path)
26
+ # Next, link up the jobs with edges
27
+ workflow.jobs.each do |job|
28
+ link_job_edges(job)
24
29
  end
30
+
31
+ format = 'png'
32
+ file_format = path.split('.')[-1]
33
+ format = file_format if file_format.length == 3
34
+
35
+ Graphviz::output(@graph, path: path, format: format)
25
36
  end
26
37
 
27
38
  def path
@@ -29,43 +40,43 @@ module Gush
29
40
  end
30
41
 
31
42
  private
32
- def add_job(graph, job)
33
- name = job.class.to_s
34
- graph.add_nodes(job.name, label: name)
43
+
44
+ def add_node(name, **specific_options)
45
+ @graph.add_node(name, **node_options.merge(specific_options))
46
+ end
47
+
48
+ def add_job_node(job)
49
+ @job_name_to_node_map[job.name] = add_node(job.name, label: node_label_for_job(job))
50
+ end
51
+
52
+ def link_job_edges(job)
53
+ job_node = @job_name_to_node_map[job.name]
35
54
 
36
55
  if job.incoming.empty?
37
- graph.add_edges(start, job.name)
56
+ @start_node.connect(job_node, **edge_options)
38
57
  end
39
58
 
40
59
  if job.outgoing.empty?
41
- graph.add_edges(job.name, end_node)
60
+ job_node.connect(@end_node, **edge_options)
42
61
  else
43
62
  job.outgoing.each do |id|
44
63
  outgoing_job = workflow.find_job(id)
45
- graph.add_edges(job.name, outgoing_job.name)
64
+ job_node.connect(@job_name_to_node_map[outgoing_job.name], **edge_options)
46
65
  end
47
66
  end
48
67
  end
49
68
 
50
- def set_node_options!(graph)
51
- node_options.each do |key, value|
52
- graph.node[key] = value
53
- end
54
- end
55
-
56
- def set_edge_options!(graph)
57
- edge_options.each do |key, value|
58
- graph.edge[key] = value
59
- end
69
+ def node_label_for_job(job)
70
+ job.class.to_s
60
71
  end
61
72
 
62
73
  def graph_options
63
74
  {
64
- type: :digraph,
65
- dpi: 200,
66
- compound: true,
67
- rankdir: "LR",
68
- center: true
75
+ dpi: 200,
76
+ compound: true,
77
+ rankdir: "LR",
78
+ center: true,
79
+ format: 'png'
69
80
  }
70
81
  end
71
82
 
data/lib/gush/worker.rb CHANGED
@@ -30,12 +30,16 @@ module Gush
30
30
 
31
31
  private
32
32
 
33
- attr_reader :client, :workflow_id, :job
33
+ attr_reader :client, :workflow_id, :job, :configuration
34
34
 
35
35
  def client
36
36
  @client ||= Gush::Client.new(Gush.configuration)
37
37
  end
38
38
 
39
+ def configuration
40
+ @configuration ||= client.configuration
41
+ end
42
+
39
43
  def setup_job(workflow_id, job_id)
40
44
  @workflow_id = workflow_id
41
45
  @job ||= client.find_job(workflow_id, job_id)
@@ -73,7 +77,11 @@ module Gush
73
77
 
74
78
  def enqueue_outgoing_jobs
75
79
  job.outgoing.each do |job_name|
76
- RedisMutex.with_lock("gush_enqueue_outgoing_jobs_#{workflow_id}-#{job_name}", sleep: 0.3, block: 2) do
80
+ RedisMutex.with_lock(
81
+ "gush_enqueue_outgoing_jobs_#{workflow_id}-#{job_name}",
82
+ sleep: configuration.polling_interval,
83
+ block: configuration.locking_duration
84
+ ) do
77
85
  out = client.find_job(workflow_id, job_name)
78
86
 
79
87
  if out.ready_to_start?
@@ -152,17 +152,15 @@ describe "Workflows" do
152
152
  flow = PayloadWorkflow.create
153
153
  flow.start!
154
154
 
155
- perform_one
156
- expect(flow.reload.find_job(flow.jobs[0].name).output_payload).to eq('first')
155
+ 3.times { perform_one }
157
156
 
158
- perform_one
159
- expect(flow.reload.find_job(flow.jobs[1].name).output_payload).to eq('second')
157
+ outputs = flow.reload.jobs.select { |j| j.klass == 'RepetitiveJob' }.map { |j| j.output_payload }
158
+ expect(outputs).to match_array(['first', 'second', 'third'])
160
159
 
161
160
  perform_one
162
- expect(flow.reload.find_job(flow.jobs[2].name).output_payload).to eq('third')
163
161
 
164
- perform_one
165
- expect(flow.reload.find_job(flow.jobs[3].name).output_payload).to eq(%w(first second third))
162
+ summary_job = flow.reload.jobs.find { |j| j.klass == 'SummaryJob' }
163
+ expect(summary_job.output_payload).to eq(%w(first second third))
166
164
  end
167
165
 
168
166
  it "does not execute `configure` on each job for huge workflows" do
@@ -8,6 +8,8 @@ describe Gush::Configuration do
8
8
  expect(subject.concurrency).to eq(5)
9
9
  expect(subject.namespace).to eq('gush')
10
10
  expect(subject.gushfile).to eq(GUSHFILE.realpath)
11
+ expect(subject.locking_duration).to eq(2)
12
+ expect(subject.polling_interval).to eq(0.3)
11
13
  end
12
14
 
13
15
  describe "#configure" do
@@ -15,10 +17,14 @@ describe Gush::Configuration do
15
17
  Gush.configure do |config|
16
18
  config.redis_url = "redis://localhost"
17
19
  config.concurrency = 25
20
+ config.locking_duration = 5
21
+ config.polling_interval = 0.5
18
22
  end
19
23
 
20
24
  expect(Gush.configuration.redis_url).to eq("redis://localhost")
21
25
  expect(Gush.configuration.concurrency).to eq(25)
26
+ expect(Gush.configuration.locking_duration).to eq(5)
27
+ expect(Gush.configuration.polling_interval).to eq(0.5)
22
28
  end
23
29
  end
24
30
  end
@@ -10,26 +10,43 @@ describe Gush::Graph do
10
10
  edge = double("edge", :[]= => true)
11
11
  graph = double("graph", node: node, edge: edge)
12
12
  path = Pathname.new(Dir.tmpdir).join(filename)
13
- expect(graph).to receive(:start).with(shape: 'diamond', fillcolor: '#CFF09E')
14
- expect(graph).to receive(:end).with(shape: 'diamond', fillcolor: '#F56991')
15
-
16
- expect(graph).to receive(:output).with(png: path.to_s)
17
-
18
- expect(graph).to receive(:add_nodes).with(/Prepare/, label: "Prepare")
19
- expect(graph).to receive(:add_nodes).with(/FetchFirstJob/, label: "FetchFirstJob")
20
- expect(graph).to receive(:add_nodes).with(/FetchSecondJob/, label: "FetchSecondJob")
21
- expect(graph).to receive(:add_nodes).with(/NormalizeJob/, label: "NormalizeJob")
22
- expect(graph).to receive(:add_nodes).with(/PersistFirstJob/, label: "PersistFirstJob")
23
-
24
- expect(graph).to receive(:add_edges).with(nil, /Prepare/)
25
- expect(graph).to receive(:add_edges).with(/Prepare/, /FetchFirstJob/)
26
- expect(graph).to receive(:add_edges).with(/Prepare/, /FetchSecondJob/)
27
- expect(graph).to receive(:add_edges).with(/FetchFirstJob/, /PersistFirstJob/)
28
- expect(graph).to receive(:add_edges).with(/FetchSecondJob/, /NormalizeJob/)
29
- expect(graph).to receive(:add_edges).with(/PersistFirstJob/, /NormalizeJob/)
30
- expect(graph).to receive(:add_edges).with(/NormalizeJob/, nil)
31
-
32
- expect(GraphViz).to receive(:new).and_yield(graph)
13
+
14
+ expect(Graphviz::Graph).to receive(:new).and_return(graph)
15
+
16
+ node_start = double('start')
17
+ node_end = double('end')
18
+ node_prepare = double('Prepare')
19
+ node_fetch_first_job = double('FetchFirstJob')
20
+ node_fetch_second_job = double('FetchSecondJob')
21
+ node_normalize_job = double('NormalizeJob')
22
+ node_persist_first_job = double('PersistFirstJob')
23
+
24
+ expect(graph).to receive(:add_node).with('start', {shape: 'diamond', fillcolor: '#CFF09E', color: "#555555", style: 'filled'}).and_return(node_start)
25
+ expect(graph).to receive(:add_node).with('end', {shape: 'diamond', fillcolor: '#F56991', color: "#555555", style: 'filled'}).and_return(node_end)
26
+
27
+ standard_options = {:color=>"#555555", :fillcolor=>"white", :label=>"Prepare", :shape=>"ellipse", :style=>"filled"}
28
+
29
+ expect(graph).to receive(:add_node).with(/Prepare/, standard_options.merge(label: "Prepare")).and_return(node_prepare)
30
+ expect(graph).to receive(:add_node).with(/FetchFirstJob/, standard_options.merge(label: "FetchFirstJob")).and_return(node_fetch_first_job)
31
+ expect(graph).to receive(:add_node).with(/FetchSecondJob/, standard_options.merge(label: "FetchSecondJob")).and_return(node_fetch_second_job)
32
+ expect(graph).to receive(:add_node).with(/NormalizeJob/, standard_options.merge(label: "NormalizeJob")).and_return(node_normalize_job)
33
+ expect(graph).to receive(:add_node).with(/PersistFirstJob/, standard_options.merge(label: "PersistFirstJob")).and_return(node_persist_first_job)
34
+
35
+ edge_options = {
36
+ dir: "forward",
37
+ penwidth: 1,
38
+ color: "#555555"
39
+ }
40
+
41
+ expect(node_start).to receive(:connect).with(node_prepare, **edge_options)
42
+ expect(node_prepare).to receive(:connect).with(node_fetch_first_job, **edge_options)
43
+ expect(node_prepare).to receive(:connect).with(node_fetch_second_job, **edge_options)
44
+ expect(node_fetch_first_job).to receive(:connect).with(node_persist_first_job, **edge_options)
45
+ expect(node_fetch_second_job).to receive(:connect).with(node_normalize_job, **edge_options)
46
+ expect(node_persist_first_job).to receive(:connect).with(node_normalize_job, **edge_options)
47
+ expect(node_normalize_job).to receive(:connect).with(node_end, **edge_options)
48
+
49
+ expect(graph).to receive(:dump_graph).and_return(nil)
33
50
 
34
51
  subject.viz
35
52
  end
@@ -4,6 +4,8 @@ describe Gush::Worker do
4
4
  subject { described_class.new }
5
5
 
6
6
  let!(:workflow) { TestWorkflow.create }
7
+ let(:locking_duration) { 5 }
8
+ let(:polling_interval) { 0.5 }
7
9
  let!(:job) { client.find_job(workflow.id, "Prepare") }
8
10
  let(:config) { Gush.configuration.to_json }
9
11
  let!(:client) { Gush::Client.new }
@@ -71,5 +73,11 @@ describe Gush::Worker do
71
73
 
72
74
  subject.perform(workflow.id, 'OkayJob')
73
75
  end
76
+
77
+ it 'calls RedisMutex.with_lock with customizable locking_duration and polling_interval' do
78
+ expect(RedisMutex).to receive(:with_lock)
79
+ .with(anything, block: 5, sleep: 0.5).twice
80
+ subject.perform(workflow.id, 'Prepare')
81
+ end
74
82
  end
75
83
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'gush'
2
- require 'fakeredis'
3
2
  require 'json'
4
3
  require 'pry'
5
4
 
@@ -35,12 +34,8 @@ class ParameterTestWorkflow < Gush::Workflow
35
34
  end
36
35
  end
37
36
 
38
- class Redis
39
- def publish(*)
40
- end
41
- end
42
37
 
43
- REDIS_URL = "redis://localhost:6379/12"
38
+ REDIS_URL = ENV["REDIS_URL"] || "redis://localhost:6379/12"
44
39
 
45
40
  module GushHelpers
46
41
  def redis
@@ -104,12 +99,13 @@ RSpec.configure do |config|
104
99
  clear_performed_jobs
105
100
 
106
101
  Gush.configure do |config|
107
- config.redis_url = REDIS_URL
108
- config.gushfile = GUSHFILE
102
+ config.redis_url = REDIS_URL
103
+ config.gushfile = GUSHFILE
104
+ config.locking_duration = defined?(locking_duration) ? locking_duration : 2
105
+ config.polling_interval = defined?(polling_interval) ? polling_interval : 0.3
109
106
  end
110
107
  end
111
108
 
112
-
113
109
  config.after(:each) do
114
110
  clear_enqueued_jobs
115
111
  clear_performed_jobs
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gush
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotrek Okoński
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-10 00:00:00.000000000 Z
11
+ date: 2022-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 4.2.7
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '7.0'
22
+ version: '7.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 4.2.7
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '7.0'
32
+ version: '7.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: concurrent-ruby
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -107,7 +107,7 @@ dependencies:
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0.6'
109
109
  - !ruby/object:Gem::Dependency
110
- name: ruby-graphviz
110
+ name: graphviz
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - "~>"
@@ -124,44 +124,56 @@ dependencies:
124
124
  name: terminal-table
125
125
  requirement: !ruby/object:Gem::Requirement
126
126
  requirements:
127
- - - "~>"
127
+ - - ">="
128
128
  - !ruby/object:Gem::Version
129
129
  version: '1.4'
130
+ - - "<"
131
+ - !ruby/object:Gem::Version
132
+ version: '3.1'
130
133
  type: :runtime
131
134
  prerelease: false
132
135
  version_requirements: !ruby/object:Gem::Requirement
133
136
  requirements:
134
- - - "~>"
137
+ - - ">="
135
138
  - !ruby/object:Gem::Version
136
139
  version: '1.4'
140
+ - - "<"
141
+ - !ruby/object:Gem::Version
142
+ version: '3.1'
137
143
  - !ruby/object:Gem::Dependency
138
- name: colorize
144
+ name: paint
139
145
  requirement: !ruby/object:Gem::Requirement
140
146
  requirements:
141
147
  - - "~>"
142
148
  - !ruby/object:Gem::Version
143
- version: '0.7'
149
+ version: '2.2'
144
150
  type: :runtime
145
151
  prerelease: false
146
152
  version_requirements: !ruby/object:Gem::Requirement
147
153
  requirements:
148
154
  - - "~>"
149
155
  - !ruby/object:Gem::Version
150
- version: '0.7'
156
+ version: '2.2'
151
157
  - !ruby/object:Gem::Dependency
152
158
  name: thor
153
159
  requirement: !ruby/object:Gem::Requirement
154
160
  requirements:
155
- - - "~>"
161
+ - - ">="
156
162
  - !ruby/object:Gem::Version
157
163
  version: '0.19'
164
+ - - "<"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.3'
158
167
  type: :runtime
159
168
  prerelease: false
160
169
  version_requirements: !ruby/object:Gem::Requirement
161
170
  requirements:
162
- - - "~>"
171
+ - - ">="
163
172
  - !ruby/object:Gem::Version
164
173
  version: '0.19'
174
+ - - "<"
175
+ - !ruby/object:Gem::Version
176
+ version: '1.3'
165
177
  - !ruby/object:Gem::Dependency
166
178
  name: launchy
167
179
  requirement: !ruby/object:Gem::Requirement
@@ -232,20 +244,6 @@ dependencies:
232
244
  - - "~>"
233
245
  - !ruby/object:Gem::Version
234
246
  version: '0.10'
235
- - !ruby/object:Gem::Dependency
236
- name: fakeredis
237
- requirement: !ruby/object:Gem::Requirement
238
- requirements:
239
- - - "~>"
240
- - !ruby/object:Gem::Version
241
- version: '0.5'
242
- type: :development
243
- prerelease: false
244
- version_requirements: !ruby/object:Gem::Requirement
245
- requirements:
246
- - - "~>"
247
- - !ruby/object:Gem::Version
248
- version: '0.5'
249
247
  description: Gush is a parallel workflow runner using Redis as storage and ActiveJob
250
248
  for executing jobs.
251
249
  email:
@@ -255,9 +253,9 @@ executables:
255
253
  extensions: []
256
254
  extra_rdoc_files: []
257
255
  files:
256
+ - ".github/workflows/ruby.yml"
258
257
  - ".gitignore"
259
258
  - ".rspec"
260
- - ".travis.yml"
261
259
  - CHANGELOG.md
262
260
  - Gemfile
263
261
  - LICENSE.txt
@@ -292,7 +290,7 @@ homepage: https://github.com/chaps-io/gush
292
290
  licenses:
293
291
  - MIT
294
292
  metadata: {}
295
- post_install_message:
293
+ post_install_message:
296
294
  rdoc_options: []
297
295
  require_paths:
298
296
  - lib
@@ -307,8 +305,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
305
  - !ruby/object:Gem::Version
308
306
  version: '0'
309
307
  requirements: []
310
- rubygems_version: 3.1.4
311
- signing_key:
308
+ rubygems_version: 3.3.3
309
+ signing_key:
312
310
  specification_version: 4
313
311
  summary: Fast and distributed workflow runner based on ActiveJob and Redis
314
312
  test_files:
data/.travis.yml DELETED
@@ -1,16 +0,0 @@
1
- language: ruby
2
- script: "bundle exec rspec"
3
- rvm:
4
- - 2.2.2
5
- - 2.3.4
6
- - 2.4.1
7
- - 2.5
8
- - 2.6
9
- - 2.7
10
- services:
11
- - redis-server
12
- email:
13
- recipients:
14
- - piotrek@okonski.org
15
- on_success: change
16
- on_failure: always