gush 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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