dynflow 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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