dynflow 1.4.3 → 1.5.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/{test/prepare_travis_env.sh → .github/install_dependencies.sh} +2 -2
  3. data/.github/workflows/ruby.yml +116 -0
  4. data/lib/dynflow.rb +1 -1
  5. data/lib/dynflow/action.rb +22 -12
  6. data/lib/dynflow/action/suspended.rb +4 -4
  7. data/lib/dynflow/action/timeouts.rb +2 -2
  8. data/lib/dynflow/actor.rb +20 -4
  9. data/lib/dynflow/clock.rb +2 -2
  10. data/lib/dynflow/connectors/abstract.rb +4 -0
  11. data/lib/dynflow/connectors/database.rb +4 -0
  12. data/lib/dynflow/connectors/direct.rb +5 -0
  13. data/lib/dynflow/director.rb +5 -1
  14. data/lib/dynflow/director/running_steps_manager.rb +2 -2
  15. data/lib/dynflow/dispatcher.rb +2 -1
  16. data/lib/dynflow/dispatcher/client_dispatcher.rb +8 -2
  17. data/lib/dynflow/dispatcher/executor_dispatcher.rb +4 -2
  18. data/lib/dynflow/execution_history.rb +1 -1
  19. data/lib/dynflow/execution_plan.rb +12 -4
  20. data/lib/dynflow/executors.rb +32 -10
  21. data/lib/dynflow/executors/abstract/core.rb +1 -1
  22. data/lib/dynflow/executors/parallel.rb +2 -2
  23. data/lib/dynflow/executors/sidekiq/orchestrator_jobs.rb +1 -1
  24. data/lib/dynflow/flows.rb +1 -0
  25. data/lib/dynflow/flows/abstract.rb +14 -0
  26. data/lib/dynflow/flows/abstract_composed.rb +2 -7
  27. data/lib/dynflow/flows/atom.rb +2 -2
  28. data/lib/dynflow/flows/concurrence.rb +2 -0
  29. data/lib/dynflow/flows/registry.rb +32 -0
  30. data/lib/dynflow/flows/sequence.rb +2 -0
  31. data/lib/dynflow/persistence.rb +8 -0
  32. data/lib/dynflow/persistence_adapters/abstract.rb +16 -0
  33. data/lib/dynflow/persistence_adapters/sequel.rb +24 -8
  34. data/lib/dynflow/persistence_adapters/sequel_migrations/020_drop_duplicate_indices.rb +30 -0
  35. data/lib/dynflow/rails.rb +1 -1
  36. data/lib/dynflow/rails/configuration.rb +16 -5
  37. data/lib/dynflow/testing/in_thread_executor.rb +2 -2
  38. data/lib/dynflow/testing/in_thread_world.rb +5 -5
  39. data/lib/dynflow/version.rb +1 -1
  40. data/lib/dynflow/world.rb +5 -5
  41. data/lib/dynflow/world/invalidation.rb +5 -1
  42. data/test/dispatcher_test.rb +6 -0
  43. data/test/flows_test.rb +44 -0
  44. data/test/future_execution_test.rb +1 -1
  45. data/test/persistence_test.rb +38 -2
  46. metadata +12 -9
  47. data/.travis.yml +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 318bdedfaca8720a62bc3a8bbd7d52b1823e9078ed1b96fecea248a729218335
4
- data.tar.gz: 35cfd900ad187608dbefaae809af955a8433a1168929d48ff11e92d164cfc8aa
3
+ metadata.gz: 5b33bcad7864102ad9f0f3bb654c7990b3037b7b0620b770d5f34f1495402855
4
+ data.tar.gz: f4089f0cb793384a764f4aa042268c19d174c989c5d348f63e126eac5fb94716
5
5
  SHA512:
6
- metadata.gz: 729dd7445b1bae4d6874749539c191912e6468cdcc120340121e9258502f8a6adc9fc7c90d043b3d4ffc6e53392910d75565233f29ce1b48448503aa366b643a
7
- data.tar.gz: 9246e6fd8a38383a5e5fd0c17d4dea5b4cc173f03b47c912673db8514cab773ae6c845c47583b85e9f8e833208751273888f73284fb4f90b451d2ed56b02998d
6
+ metadata.gz: c119f0ce0d3605012206b083b829bc85194d676f762d61a394be6257d376c56388efd72f4a77f78445ece78942c5eb561728031e9a703767113eab7780b9d0e3
7
+ data.tar.gz: 136dafb81d0766e9c1041a9a52896b4cbe0d300482be729900fd9fe8f83e6095bbc084a6d43d0aa9012b436544690d3c2d774d46f0b9c573775b3707daec3428
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
+ set -x
4
+
3
5
  echo "Setting the environment to use ${DB} database"
4
6
 
5
7
  BUNDLE_CONFIG=.bundle/config
@@ -12,11 +14,9 @@ EOF
12
14
  case $DB in
13
15
  mysql)
14
16
  sed -i 's/:mysql//'g $BUNDLE_CONFIG
15
- mysql -e 'create database travis_ci_test;'
16
17
  ;;
17
18
  postgresql)
18
19
  sed -i 's/:postgresql//'g $BUNDLE_CONFIG
19
- psql -c 'create database travis_ci_test;' -U postgres
20
20
  ;;
21
21
  sqlite3)
22
22
  # the tests are by default using sqlite3: do nothing
@@ -0,0 +1,116 @@
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: [pull_request]
11
+
12
+ env:
13
+ TESTOPTS: --verbose
14
+
15
+ jobs:
16
+ rubocop:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Setup Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: 2.7
24
+ - name: Setup
25
+ run: |
26
+ gem install bundler
27
+ bundle install --jobs=3 --retry=3
28
+ - name: Run rubocop
29
+ run: bundle exec rubocop
30
+
31
+ test:
32
+ runs-on: ubuntu-latest
33
+ needs: rubocop
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ ruby_version:
38
+ - 2.5.0
39
+ - 2.6.0
40
+ - 2.7.0
41
+ - 3.0.0
42
+ concurrent_ruby_ext:
43
+ - 'true'
44
+ - 'false'
45
+ db:
46
+ - postgresql
47
+ - mysql
48
+ - sqlite3
49
+ include:
50
+ - db: postgresql
51
+ conn_string: postgres://postgres@localhost/travis_ci_test
52
+ - db: mysql
53
+ conn_string: mysql2://root@127.0.0.1/travis_ci_test
54
+ - db: sqlite3
55
+ conn_string: sqlite:/
56
+ exclude:
57
+ - db: mysql
58
+ ruby_version: 2.5.0
59
+ - db: mysql
60
+ ruby_version: 2.6.0
61
+ - db: mysql
62
+ ruby_version: 3.0.0
63
+ - db: mysql
64
+ concurrent_ruby_ext: 'true'
65
+ - db: sqlite3
66
+ ruby_version: 2.5.0
67
+ - db: sqlite3
68
+ ruby_version: 2.6.0
69
+ - db: sqlite3
70
+ ruby_version: 3.0.0
71
+ - db: sqlite3
72
+ concurrent_ruby_ext: 'true'
73
+ - db: postgresql
74
+ ruby_version: 2.5.0
75
+ concurrent_ruby_ext: 'true'
76
+ - db: postgresql
77
+ ruby_version: 2.6.0
78
+ concurrent_ruby_ext: 'true'
79
+ - db: postgresql
80
+ ruby_version: 3.0.0
81
+ concurrent_ruby_ext: 'true'
82
+
83
+ services:
84
+ postgres:
85
+ image: postgres:12.1
86
+ ports: ['5432:5432']
87
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
88
+ env:
89
+ POSTGRES_DB: travis_ci_test
90
+ mariadb:
91
+ image: mariadb:10
92
+ ports: ['3306:3306']
93
+ env:
94
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
95
+ MYSQL_DATABASE: travis_ci_test
96
+ redis:
97
+ image: redis:latest
98
+ ports: ['6379:6379']
99
+
100
+ env:
101
+ DB: ${{ matrix.db }}
102
+ DB_CONN_STRING: ${{ matrix.conn_string }}
103
+ CONCURRENT_RUBY_EXT: "${{ matrix.concurrent_ruby_ext }}"
104
+
105
+ steps:
106
+ - uses: actions/checkout@v2
107
+ - name: Set up Ruby
108
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
109
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
110
+ uses: ruby/setup-ruby@v1
111
+ with:
112
+ ruby-version: ${{ matrix.ruby_version }}
113
+ - name: Install dependencies
114
+ run: .github/install_dependencies.sh
115
+ - name: Run tests
116
+ run: bundle exec rake test
data/lib/dynflow.rb CHANGED
@@ -76,7 +76,7 @@ module Dynflow
76
76
  if defined? ::ActiveJob
77
77
  require 'dynflow/active_job/queue_adapter'
78
78
 
79
- class Railtie < Rails::Railtie
79
+ class Railtie < ::Rails::Railtie
80
80
  config.before_initialize do
81
81
  ::ActiveJob::QueueAdapters.send(
82
82
  :include,
@@ -93,7 +93,8 @@ module Dynflow
93
93
  fields! execution_plan_id: String,
94
94
  step_id: Integer,
95
95
  event: Object,
96
- time: type { variants Time, NilClass }
96
+ time: type { variants Time, NilClass },
97
+ optional: Algebrick::Types::Boolean
97
98
  end
98
99
 
99
100
  def self.constantize(action_name)
@@ -332,9 +333,9 @@ module Dynflow
332
333
 
333
334
  # Plan an +event+ to be send to the action defined by +action+, what defaults to be self.
334
335
  # if +time+ is not passed, event is sent as soon as possible.
335
- def plan_event(event, time = nil, execution_plan_id: self.execution_plan_id, step_id: self.run_step_id)
336
+ def plan_event(event, time = nil, execution_plan_id: self.execution_plan_id, step_id: self.run_step_id, optional: false)
336
337
  time = @world.clock.current_time + time if time.is_a?(Numeric)
337
- delayed_events << DelayedEvent[execution_plan_id, step_id, event, time]
338
+ delayed_events << DelayedEvent[execution_plan_id, step_id, event, time, optional]
338
339
  end
339
340
 
340
341
  def delayed_events
@@ -352,15 +353,12 @@ module Dynflow
352
353
  @step.state = state
353
354
  end
354
355
 
356
+ # If this save returns an integer, it means it was an update. The number
357
+ # represents the number of updated records. If it is 0, then the step was in
358
+ # an unexpected state and couldn't be updated
355
359
  def save_state(conditions = {})
356
360
  phase! Executable
357
- # If this save returns an integer, it means it was an update. The number
358
- # represents the number of updated records. If it is 0, then the step
359
- # was in an unexpected state and couldn't be updated, in which case we
360
- # raise an exception and crash hard to prevent the step from being
361
- # executed twice
362
- count = @step.save(conditions)
363
- raise 'Could not save state' if count.kind_of?(Integer) && !count.positive?
361
+ @step.save(conditions)
364
362
  end
365
363
 
366
364
  def delay(delay_options, *args)
@@ -536,11 +534,11 @@ module Dynflow
536
534
  end
537
535
 
538
536
  # TODO: This is getting out of hand, refactoring needed
537
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
539
538
  def execute_run(event)
540
539
  phase! Run
541
540
  @world.logger.debug format('%13s %s:%2d got event %s',
542
541
  'Step', execution_plan_id, @step.id, event) if event
543
- @input = OutputReference.dereference @input, world.persistence
544
542
 
545
543
  case
546
544
  when state == :running
@@ -551,8 +549,19 @@ module Dynflow
551
549
  raise 'event can be processed only when in suspended state'
552
550
  end
553
551
 
552
+ old_state = self.state
554
553
  self.state = :running unless self.state == :skipping
555
- save_state(:state => %w(pending error skipping suspended))
554
+ saved = save_state(:state => %w(pending error skipping suspended))
555
+ if saved.kind_of?(Integer) && !saved.positive?
556
+ # The step was already in a state we're trying to transition to, most
557
+ # likely we were about to execute it for the second time after first
558
+ # execution was forcefully interrupted.
559
+ # Set error and return to prevent the step from being executed twice
560
+ set_error "Could not transition step from #{old_state} to #{self.state}, step already in #{self.state}."
561
+ return
562
+ end
563
+
564
+ @input = OutputReference.dereference @input, world.persistence
556
565
  with_error_handling do
557
566
  event = Skip if state == :skipping
558
567
 
@@ -573,6 +582,7 @@ module Dynflow
573
582
  raise "wrong state #{state} when event:#{event}"
574
583
  end
575
584
  end
585
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
576
586
 
577
587
  def execute_finalize
578
588
  phase! Finalize
@@ -9,14 +9,14 @@ module Dynflow
9
9
  @step_id = action.run_step_id
10
10
  end
11
11
 
12
- def plan_event(event, time, sent = Concurrent::Promises.resolvable_future)
13
- @world.plan_event(execution_plan_id, step_id, event, time, sent)
12
+ def plan_event(event, time, sent = Concurrent::Promises.resolvable_future, optional: false)
13
+ @world.plan_event(execution_plan_id, step_id, event, time, sent, optional: optional)
14
14
  end
15
15
 
16
- def event(event, sent = Concurrent::Promises.resolvable_future)
16
+ def event(event, sent = Concurrent::Promises.resolvable_future, optional: false)
17
17
  # TODO: deprecate 2 levels backtrace (to know it's called from clock or internaly)
18
18
  # remove lib/dynflow/clock.rb ClockReference#ping branch condition on removal.
19
- plan_event(event, nil, sent)
19
+ plan_event(event, nil, sent, optional: optional)
20
20
  end
21
21
 
22
22
  def <<(event = nil)
@@ -7,8 +7,8 @@ module Dynflow
7
7
  fail("Timeout exceeded.")
8
8
  end
9
9
 
10
- def schedule_timeout(seconds)
11
- plan_event(Timeout, seconds)
10
+ def schedule_timeout(seconds, optional: false)
11
+ plan_event(Timeout, seconds, optional: optional)
12
12
  end
13
13
  end
14
14
  end
data/lib/dynflow/actor.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  module Dynflow
3
3
 
4
+ FULL_BACKTRACE = %w[1 y yes].include?((ENV['DYNFLOW_FULL_BACKTRACE'] || '').downcase)
5
+ BACKTRACE_LIMIT = begin
6
+ limit = ENV['DYNFLOW_BACKTRACE_LIMIT'].to_i
7
+ limit.zero? ? nil : limit
8
+ end
9
+
4
10
  module MethodicActor
5
11
  def on_message(message)
6
12
  method, *args = message
@@ -44,7 +50,11 @@ module Dynflow
44
50
  include LogWithFullBacktrace
45
51
 
46
52
  def on_envelope(envelope)
47
- Actor::BacktraceCollector.with_backtrace(envelope.origin_backtrace) { super }
53
+ if FULL_BACKTRACE
54
+ Actor::BacktraceCollector.with_backtrace(envelope.origin_backtrace) { super }
55
+ else
56
+ super
57
+ end
48
58
  end
49
59
  end
50
60
 
@@ -83,9 +93,15 @@ module Dynflow
83
93
 
84
94
  # takes an array of backtrace lines and replaces each chunk
85
95
  def filter_backtrace(backtrace)
86
- backtrace.map { |line| filter_line(line) }
87
- .chunk_while { |l1, l2| l1 == l2}
88
- .map(&:first)
96
+ trace = backtrace.map { |line| filter_line(line) }
97
+ .chunk_while { |l1, l2| l1 == l2}
98
+ .map(&:first)
99
+ if BACKTRACE_LIMIT
100
+ count = trace.count
101
+ trace = trace.take(BACKTRACE_LIMIT)
102
+ trace << "[ backtrace omitted #{count - BACKTRACE_LIMIT} lines ]" if trace.count < count
103
+ end
104
+ trace
89
105
  end
90
106
  end
91
107
  end
data/lib/dynflow/clock.rb CHANGED
@@ -114,11 +114,11 @@ module Dynflow
114
114
  Time.now
115
115
  end
116
116
 
117
- def ping(who, time, with_what = nil, where = :<<)
117
+ def ping(who, time, with_what = nil, where = :<<, optional: false)
118
118
  Type! time, Time, Numeric
119
119
  time = current_time + time if time.is_a? Numeric
120
120
  if who.is_a?(Action::Suspended)
121
- who.plan_event(with_what, time)
121
+ who.plan_event(with_what, time, optional: optional)
122
122
  else
123
123
  timer = Clock::Timer[who, time, with_what.nil? ? Algebrick::Types::None : Some[Object][with_what], where]
124
124
  self.tell([:add_timer, timer])
@@ -25,6 +25,10 @@ module Dynflow
25
25
  raise NotImplementedError
26
26
  end
27
27
 
28
+ def prune_undeliverable_envelopes(world)
29
+ raise NotImplementedError
30
+ end
31
+
28
32
  # we need to pass the world, as the connector can be shared
29
33
  # between words: we need to know the one to send the message to
30
34
  def receive(world, envelope)
@@ -172,6 +172,10 @@ module Dynflow
172
172
  Telemetry.with_instance { |t| t.increment_counter(:dynflow_connector_envelopes, 1, :world => envelope.sender_id, :direction => 'outgoing') }
173
173
  @core.ask([:handle_envelope, envelope])
174
174
  end
175
+
176
+ def prune_undeliverable_envelopes(world)
177
+ world.persistence.prune_undeliverable_envelopes
178
+ end
175
179
  end
176
180
  end
177
181
  end
@@ -68,6 +68,11 @@ module Dynflow
68
68
  Telemetry.with_instance { |t| t.increment_counter(:dynflow_connector_envelopes, 1, :world => envelope.sender_id) }
69
69
  @core.ask([:handle_envelope, envelope])
70
70
  end
71
+
72
+ def prune_undeliverable_envelopes(_world)
73
+ # This is a noop
74
+ 0
75
+ end
71
76
  end
72
77
  end
73
78
  end
@@ -15,7 +15,8 @@ module Dynflow
15
15
  execution_plan_id: String,
16
16
  step_id: Integer,
17
17
  event: Object,
18
- result: Concurrent::Promises::ResolvableFuture
18
+ result: Concurrent::Promises::ResolvableFuture,
19
+ optional: Algebrick::Types::Boolean
19
20
  end
20
21
 
21
22
  UnprocessableEvent = Class.new(Dynflow::Error)
@@ -163,6 +164,9 @@ module Dynflow
163
164
  execution_plan_manager = @execution_plan_managers[event.execution_plan_id]
164
165
  if execution_plan_manager
165
166
  execution_plan_manager.event(event)
167
+ elsif event.optional
168
+ event.result.reject "no manager for #{event.inspect}"
169
+ []
166
170
  else
167
171
  raise Dynflow::Error, "no manager for #{event.inspect}"
168
172
  end
@@ -20,8 +20,8 @@ module Dynflow
20
20
  def terminate
21
21
  pending_work = @work_items.clear.values.flatten(1)
22
22
  pending_work.each do |w|
23
- if EventWorkItem === w
24
- w.event.result.reject UnprocessableEvent.new("dropping due to termination")
23
+ finish_event_result(w) do |result|
24
+ result.reject UnprocessableEvent.new("dropping due to termination")
25
25
  end
26
26
  end
27
27
  end
@@ -6,7 +6,8 @@ module Dynflow
6
6
  fields! execution_plan_id: String,
7
7
  step_id: Integer,
8
8
  event: Object,
9
- time: type { variants Time, NilClass }
9
+ time: type { variants Time, NilClass },
10
+ optional: Algebrick::Types::Boolean
10
11
  end
11
12
 
12
13
  Execution = type do
@@ -132,11 +132,13 @@ module Dynflow
132
132
  end
133
133
 
134
134
  def dispatch_request(request, client_world_id, request_id)
135
+ ignore_unknown = false
135
136
  executor_id = match request,
136
137
  (on ~Execution do |execution|
137
138
  AnyExecutor
138
139
  end),
139
140
  (on ~Event do |event|
141
+ ignore_unknown = event.optional
140
142
  find_executor(event.execution_plan_id)
141
143
  end),
142
144
  (on Ping.(~any, ~any) | Status.(~any, ~any) do |receiver_id, _|
@@ -144,7 +146,11 @@ module Dynflow
144
146
  end)
145
147
  envelope = Envelope[request_id, client_world_id, executor_id, request]
146
148
  if Dispatcher::UnknownWorld === envelope.receiver_id
147
- raise Dynflow::Error, "Could not find an executor for #{envelope}"
149
+ raise Dynflow::Error, "Could not find an executor for #{envelope}" unless ignore_unknown
150
+
151
+ message = "Could not find an executor for optional #{envelope}, discarding."
152
+ log(Logger::DEBUG, message)
153
+ return respond(envelope, Failed[message])
148
154
  end
149
155
  connector.send(envelope).value!
150
156
  rescue => e
@@ -252,7 +258,7 @@ module Dynflow
252
258
  future.fulfill(true)
253
259
  else
254
260
  if @ping_cache.executor?(request.receiver_id)
255
- future.reject
261
+ future.reject false
256
262
  else
257
263
  yield
258
264
  end