dynflow 1.4.1 → 1.4.6

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: 727176abc78b46f7b0f0381806561496fba8cdb9e31c5cdea473b5bd6de7bcf0
4
- data.tar.gz: aa1d255d09123e50d1d7c718c4738e87fa88741ad23e1f548f0444bdb98fc534
3
+ metadata.gz: 1d3c22d2470835d38bb89ac45879837ac515268c40f919eb3b57de809cb912f4
4
+ data.tar.gz: 8552edbffe0f91aa162b5b004d1ef40046f719dee746a2e2cf3da119786ac0d9
5
5
  SHA512:
6
- metadata.gz: 98f12bd5c1cd06b925601b6c9e6fc5dda7f2f81ecba805251cfb987c9b131bd88be59f57098d8bab4c856b33743815fa2338524ffe3804d4d1dda0e44f73fadc
7
- data.tar.gz: 8747b3e08c259a94dd025acb4cfc97654ff8c1ec5782b8bf7876b455187fb789f91d467e1ca9468e48c1a16d745fe3279911652f669b84f4864df8af3066b3cf
6
+ metadata.gz: cbc4d8031d1836644d0327b2afb83d95f0ada20955e6e5c60bde8de447a2cc83bb2b3bcb116a2e9eb4303ed54bef55ca31c4098d6e11003b3a3717d3f96127ec
7
+ data.tar.gz: 48db612dd381a90474caac16c524304d15862d8947314abc21dd768b481402dae1c8785377eaf11cc9f9674520ec9061b279312fccc98d5aa932c00cf66284ce
@@ -2,6 +2,7 @@ language: ruby
2
2
 
3
3
  services:
4
4
  - postgresql
5
+ - redis
5
6
 
6
7
  rvm:
7
8
  - "2.3.1"
@@ -18,6 +19,7 @@ matrix:
18
19
  env: "DB=mysql DB_CONN_STRING=mysql2://root@localhost/travis_ci_test"
19
20
  services:
20
21
  - mysql
22
+ - redis
21
23
  - rvm: "2.4.0"
22
24
  env: "DB=sqlite3 DB_CONN_STRING=sqlite:/"
23
25
  - rvm: "2.4.0"
@@ -23,13 +23,14 @@ Gem::Specification.new do |s|
23
23
  s.add_dependency "apipie-params"
24
24
  s.add_dependency "algebrick", '~> 0.7.0'
25
25
  s.add_dependency "concurrent-ruby", '~> 1.1.3'
26
- s.add_dependency "concurrent-ruby-edge", '~> 0.4.1'
26
+ s.add_dependency "concurrent-ruby-edge", '~> 0.6.0'
27
27
  s.add_dependency "sequel", '>= 4.0.0'
28
28
 
29
29
  s.add_development_dependency "rake"
30
30
  s.add_development_dependency "rack-test"
31
31
  s.add_development_dependency "minitest"
32
32
  s.add_development_dependency "minitest-reporters"
33
+ s.add_development_dependency "minitest-stub-const"
33
34
  s.add_development_dependency "activerecord"
34
35
  s.add_development_dependency 'activejob'
35
36
  s.add_development_dependency "sqlite3"
@@ -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
@@ -4,16 +4,38 @@ module Dynflow
4
4
 
5
5
  require 'dynflow/executors/parallel'
6
6
 
7
- # Every time we run a code that can be defined outside of Dynflow,
8
- # we should wrap it with this method, and we can ensure here to do
9
- # necessary cleanup, such as cleaning ActiveRecord connections
10
- def self.run_user_code
11
- clear_connections = defined?(::ActiveRecord) && ActiveRecord::Base.connected? && ActiveRecord::Base.connection.open_transactions.zero?
12
- yield
13
- ensure
14
- ::ActiveRecord::Base.clear_active_connections! if clear_connections
15
- ::Logging.mdc.clear if defined? ::Logging
16
- end
7
+ class << self
8
+ # Every time we run a code that can be defined outside of Dynflow,
9
+ # we should wrap it with this method, and we can ensure here to do
10
+ # necessary cleanup, such as cleaning ActiveRecord connections
11
+ def run_user_code
12
+ # Here we cover a case where the connection was already checked out from
13
+ # the pool and had opened transactions. In that case, we should leave the
14
+ # cleanup to the other runtime unit which opened the transaction. If the
15
+ # connection was checked out or there are no opened transactions, we can
16
+ # safely perform the cleanup.
17
+ no_previously_opened_transactions = active_record_open_transactions.zero?
18
+ yield
19
+ ensure
20
+ ::ActiveRecord::Base.clear_active_connections! if no_previously_opened_transactions && active_record_connected?
21
+ ::Logging.mdc.clear if defined? ::Logging
22
+ end
23
+
24
+ private
25
+
26
+ def active_record_open_transactions
27
+ active_record_active_connection&.open_transactions || 0
28
+ end
17
29
 
30
+ def active_record_active_connection
31
+ return unless defined?(::ActiveRecord) && ::ActiveRecord::Base.connected?
32
+ # #active_connection? returns the connection if already established or nil
33
+ ::ActiveRecord::Base.connection_pool.active_connection?
34
+ end
35
+
36
+ def active_record_connected?
37
+ !!active_record_active_connection
38
+ end
39
+ end
18
40
  end
19
41
  end
@@ -13,7 +13,7 @@ module Dynflow
13
13
  def perform(work_item, delayed_events = nil)
14
14
  # Usually the step is saved on the worker's side. However if sidekiq is shut down,
15
15
  # then the step may not have been saved so we save it just to be sure
16
- if work_item.is_a?(Director::StepWorkItem) && work_item.step&.error&.exception.is_a?(::Sidekiq::Shutdown)
16
+ if work_item.is_a?(Director::StepWorkItem) && work_item.step&.error&.exception_class == ::Sidekiq::Shutdown
17
17
  work_item.step.save
18
18
  end
19
19
  Dynflow.process_world.executor.core.tell([:work_finished, work_item, delayed_events])
@@ -134,5 +134,13 @@ module Dynflow
134
134
  envelope
135
135
  end
136
136
  end
137
+
138
+ def prune_envelopes(receiver_ids)
139
+ adapter.prune_envelopes(receiver_ids)
140
+ end
141
+
142
+ def prune_undeliverable_envelopes
143
+ adapter.prune_undeliverable_envelopes
144
+ end
137
145
  end
138
146
  end
@@ -116,6 +116,22 @@ module Dynflow
116
116
  def push_envelope(envelope)
117
117
  raise NotImplementedError
118
118
  end
119
+
120
+ def prune_envelopes(receiver_ids)
121
+ raise NotImplementedError
122
+ end
123
+
124
+ def prune_undeliverable_envelopes
125
+ raise NotImplementedError
126
+ end
127
+
128
+ def migrate_db
129
+ raise NotImplementedError
130
+ end
131
+
132
+ def abort_if_pending_migrations!
133
+ raise NotImplementedError
134
+ end
119
135
  end
120
136
  end
121
137
  end
@@ -45,13 +45,15 @@ module Dynflow
45
45
  step: %w(error children) }
46
46
 
47
47
  def initialize(config)
48
+ migrate = true
48
49
  config = config.dup
49
50
  @additional_responsibilities = { coordinator: true, connector: true }
50
- if config.is_a?(Hash) && config.key?(:additional_responsibilities)
51
- @additional_responsibilities.merge!(config.delete(:additional_responsibilities))
51
+ if config.is_a?(Hash)
52
+ @additional_responsibilities.merge!(config.delete(:additional_responsibilities)) if config.key?(:additional_responsibilities)
53
+ migrate = config.fetch(:migrate, true)
52
54
  end
53
55
  @db = initialize_db config
54
- migrate_db
56
+ migrate_db if migrate
55
57
  end
56
58
 
57
59
  def transaction(&block)
@@ -198,6 +200,16 @@ module Dynflow
198
200
  table(:envelope).insert(prepare_record(:envelope, envelope))
199
201
  end
200
202
 
203
+ def prune_envelopes(receiver_ids)
204
+ connector_feature!
205
+ table(:envelope).where(receiver_id: receiver_ids).delete
206
+ end
207
+
208
+ def prune_undeliverable_envelopes
209
+ connector_feature!
210
+ table(:envelope).where(receiver_id: table(:coordinator_record).select(:id)).invert.delete
211
+ end
212
+
201
213
  def coordinator_feature!
202
214
  unless @additional_responsibilities[:coordinator]
203
215
  raise "The sequel persistence adapter coordinator feature used but not enabled in additional_features"
@@ -238,6 +250,14 @@ module Dynflow
238
250
  envelopes: table(:envelope).all.to_a }
239
251
  end
240
252
 
253
+ def migrate_db
254
+ ::Sequel::Migrator.run(db, self.class.migrations_path, table: 'dynflow_schema_info')
255
+ end
256
+
257
+ def abort_if_pending_migrations!
258
+ ::Sequel::Migrator.check_current(db, self.class.migrations_path, table: 'dynflow_schema_info')
259
+ end
260
+
241
261
  private
242
262
 
243
263
  TABLES = { execution_plan: :dynflow_execution_plans,
@@ -259,10 +279,6 @@ module Dynflow
259
279
  File.expand_path('../sequel_migrations', __FILE__)
260
280
  end
261
281
 
262
- def migrate_db
263
- ::Sequel::Migrator.run(db, self.class.migrations_path, table: 'dynflow_schema_info')
264
- end
265
-
266
282
  def prepare_record(table_name, value, base = {}, with_data = true)
267
283
  record = base.dup
268
284
  if with_data && table(table_name).columns.include?(:data)
@@ -32,7 +32,8 @@ Sequel.migration do
32
32
  primary_key [:execution_plan_uuid, :id]
33
33
  index [:execution_plan_uuid, :id], :unique => true
34
34
  column :action_id, Integer
35
- foreign_key [:execution_plan_uuid, :action_id], :dynflow_actions
35
+ foreign_key [:execution_plan_uuid, :action_id], :dynflow_actions,
36
+ name: :dynflow_steps_execution_plan_uuid_fkey1
36
37
  index [:execution_plan_uuid, :action_id]
37
38
 
38
39
  column :data, String, text: true
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ Sequel.migration do
3
+ up do
4
+ alter_table(:dynflow_actions) do
5
+ drop_index [:execution_plan_uuid, :id]
6
+ end
7
+
8
+ alter_table(:dynflow_execution_plans) do
9
+ drop_index :uuid
10
+ end
11
+
12
+ alter_table(:dynflow_steps) do
13
+ drop_index [:execution_plan_uuid, :id]
14
+ end
15
+ end
16
+
17
+ down do
18
+ alter_table(:dynflow_actions) do
19
+ add_index [:execution_plan_uuid, :id], :unique => true
20
+ end
21
+
22
+ alter_table(:dynflow_execution_plans) do
23
+ add_index :uuid, :unique => true
24
+ end
25
+
26
+ alter_table(:dynflow_steps) do
27
+ add_index [:execution_plan_uuid, :id], :unique => true
28
+ end
29
+ end
30
+ end
@@ -100,8 +100,15 @@ module Dynflow
100
100
  end
101
101
 
102
102
  def calculate_db_pool_size(world)
103
- self.db_pool_size || world.config.queues.values.inject(5) do |pool_size, pool_options|
104
- pool_size += pool_options[:pool_size]
103
+ return self.db_pool_size if self.db_pool_size
104
+
105
+ base_value = 5
106
+ if defined?(::Sidekiq)
107
+ Sidekiq.options[:concurrency] + base_value
108
+ else
109
+ world.config.queues.values.inject(base_value) do |pool_size, pool_options|
110
+ pool_size += pool_options[:pool_size]
111
+ end
105
112
  end
106
113
  end
107
114
 
@@ -185,8 +192,8 @@ module Dynflow
185
192
  end
186
193
 
187
194
  # Sequel adapter based on Rails app database.yml configuration
188
- def initialize_persistence(world)
189
- persistence_class.new(default_sequel_adapter_options(world))
195
+ def initialize_persistence(world, options = {})
196
+ persistence_class.new(default_sequel_adapter_options(world).merge(options))
190
197
  end
191
198
  end
192
199
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Dynflow
3
- VERSION = '1.4.1'
3
+ VERSION = '1.4.6'
4
4
  end
@@ -341,7 +341,7 @@ module Dynflow
341
341
  @terminating = Concurrent::Promises.future do
342
342
  termination_future.wait(termination_timeout)
343
343
  end.on_resolution do
344
- @terminated.complete
344
+ @terminated.resolve
345
345
  Thread.new { Kernel.exit } if @exit_on_terminate.true?
346
346
  end
347
347
  end
@@ -28,6 +28,8 @@ module Dynflow
28
28
  end
29
29
  end
30
30
 
31
+ pruned = persistence.prune_envelopes(world.id)
32
+ logger.error("Pruned #{pruned} envelopes for invalidated world #{world.id}") unless pruned.zero?
31
33
  coordinator.delete_world(world)
32
34
  end
33
35
  end
@@ -115,6 +117,8 @@ module Dynflow
115
117
  def perform_validity_checks
116
118
  world_invalidation_result = worlds_validity_check
117
119
  locks_validity_check
120
+ pruned = connector.prune_undeliverable_envelopes(self)
121
+ logger.error("Pruned #{pruned} undeliverable envelopes") unless pruned.zero?
118
122
  world_invalidation_result.values.select { |result| result == :invalidated }.size
119
123
  end
120
124
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative 'test_helper'
3
3
  require 'tmpdir'
4
+ require 'ostruct'
4
5
 
5
6
  module Dynflow
6
7
  module PersistenceTest
@@ -371,6 +372,41 @@ module Dynflow
371
372
  assert_equal [], adapter.pull_envelopes(executor_world_id)
372
373
  end
373
374
 
375
+ it 'supports pruning of envelopes of invalidated worlds' do
376
+ client_world_id = '5678'
377
+ executor_world_id = '1234'
378
+ envelope_hash = ->(envelope) { Dynflow::Utils.indifferent_hash(Dynflow.serializer.dump(envelope)) }
379
+ executor_envelope = envelope_hash.call(Dispatcher::Envelope['123', client_world_id, executor_world_id, Dispatcher::Execution['111']])
380
+ client_envelope = envelope_hash.call(Dispatcher::Envelope['123', executor_world_id, client_world_id, Dispatcher::Accepted])
381
+ envelopes = [client_envelope, executor_envelope]
382
+
383
+ envelopes.each { |e| adapter.push_envelope(e) }
384
+
385
+ assert_equal 1, adapter.prune_envelopes([executor_world_id])
386
+ assert_equal 0, adapter.prune_envelopes([executor_world_id])
387
+ assert_equal [], adapter.pull_envelopes(executor_world_id)
388
+ assert_equal [client_envelope], adapter.pull_envelopes(client_world_id)
389
+ end
390
+
391
+ it 'supports pruning of orphaned envelopes' do
392
+ client_world_id = '5678'
393
+ executor_world_id = '1234'
394
+ envelope_hash = ->(envelope) { Dynflow::Utils.indifferent_hash(Dynflow.serializer.dump(envelope)) }
395
+ executor_envelope = envelope_hash.call(Dispatcher::Envelope['123', client_world_id, executor_world_id, Dispatcher::Execution['111']])
396
+ client_envelope = envelope_hash.call(Dispatcher::Envelope['123', executor_world_id, client_world_id, Dispatcher::Accepted])
397
+ envelopes = [client_envelope, executor_envelope]
398
+
399
+ envelopes.each { |e| adapter.push_envelope(e) }
400
+ adapter.insert_coordinator_record({"class"=>"Dynflow::Coordinator::ExecutorWorld",
401
+ "id" => executor_world_id, "meta" => {}, "active" => true })
402
+
403
+ assert_equal 1, adapter.prune_undeliverable_envelopes
404
+ assert_equal 0, adapter.prune_undeliverable_envelopes
405
+ assert_equal [], adapter.pull_envelopes(client_world_id)
406
+ assert_equal [executor_envelope], adapter.pull_envelopes(executor_world_id)
407
+ assert_equal [], adapter.pull_envelopes(executor_world_id)
408
+ end
409
+
374
410
  it 'supports reading data saved prior to normalization' do
375
411
  db = adapter.send(:db)
376
412
  # Prepare records for saving
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'test_helper'
3
+ require 'mocha/minitest'
4
+ require 'minitest/stub_const'
5
+ require 'ostruct'
6
+ require 'sidekiq'
7
+ require 'dynflow/executors/sidekiq/core'
8
+
9
+ module Dynflow
10
+ module RedisLockingTest
11
+ describe Executors::Sidekiq::RedisLocking do
12
+ class Orchestrator
13
+ include Executors::Sidekiq::RedisLocking
14
+
15
+ attr_accessor :logger
16
+
17
+ def initialize(world, logger)
18
+ @world = world
19
+ @logger = logger
20
+ end
21
+ end
22
+
23
+ class Logger
24
+ attr_reader :logs
25
+ def initialize
26
+ @logs = []
27
+ end
28
+
29
+ [:info, :error, :fatal].each do |key|
30
+ define_method key do |message|
31
+ @logs << [key, message]
32
+ end
33
+ end
34
+ end
35
+
36
+ after do
37
+ ::Sidekiq.redis do |conn|
38
+ conn.del Executors::Sidekiq::RedisLocking::REDIS_LOCK_KEY
39
+ end
40
+ end
41
+
42
+ def redis_orchestrator_id
43
+ ::Sidekiq.redis do |conn|
44
+ conn.get Executors::Sidekiq::RedisLocking::REDIS_LOCK_KEY
45
+ end
46
+ end
47
+
48
+ let(:world) { OpenStruct.new(:id => '12345') }
49
+ let(:world2) { OpenStruct.new(:id => '67890') }
50
+ let(:orchestrator) { Orchestrator.new(world, Logger.new) }
51
+ let(:orchestrator2) { Orchestrator.new(world2, Logger.new) }
52
+
53
+ it 'acquires the lock when it is not taken' do
54
+ orchestrator.wait_for_orchestrator_lock
55
+ logs = orchestrator.logger.logs
56
+ _(redis_orchestrator_id).must_equal world.id
57
+ _(logs).must_equal [[:info, 'Acquired orchestrator lock, entering active mode.']]
58
+ end
59
+
60
+ it 'reacquires the lock if it was lost' do
61
+ orchestrator.reacquire_orchestrator_lock
62
+ logs = orchestrator.logger.logs
63
+ _(redis_orchestrator_id).must_equal world.id
64
+ _(logs).must_equal [[:error, 'The orchestrator lock was lost, reacquired']]
65
+ end
66
+
67
+ it 'terminates the process if lock was stolen' do
68
+ orchestrator.wait_for_orchestrator_lock
69
+ Process.expects(:kill)
70
+ orchestrator2.reacquire_orchestrator_lock
71
+ logs = orchestrator2.logger.logs
72
+ _(redis_orchestrator_id).must_equal world.id
73
+ _(logs).must_equal [[:fatal, 'The orchestrator lock was stolen by 12345, aborting.']]
74
+ end
75
+
76
+ it 'polls for the lock availability' do
77
+ Executors::Sidekiq::RedisLocking.stub_const(:REDIS_LOCK_TTL, 1) do
78
+ Executors::Sidekiq::RedisLocking.stub_const(:REDIS_LOCK_POLL_INTERVAL, 0.5) do
79
+ orchestrator.wait_for_orchestrator_lock
80
+ _(redis_orchestrator_id).must_equal world.id
81
+ orchestrator2.wait_for_orchestrator_lock
82
+ end
83
+ end
84
+
85
+ _(redis_orchestrator_id).must_equal world2.id
86
+ passive, active = orchestrator2.logger.logs
87
+ _(passive).must_equal [:info, 'Orchestrator lock already taken, entering passive mode.']
88
+ _(active).must_equal [:info, 'Acquired orchestrator lock, entering active mode.']
89
+ end
90
+ end
91
+ end
92
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Necas
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-25 00:00:00.000000000 Z
12
+ date: 2020-06-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_json
@@ -73,14 +73,14 @@ dependencies:
73
73
  requirements:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: 0.4.1
76
+ version: 0.6.0
77
77
  type: :runtime
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
- version: 0.4.1
83
+ version: 0.6.0
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: sequel
86
86
  requirement: !ruby/object:Gem::Requirement
@@ -151,6 +151,20 @@ dependencies:
151
151
  - - ">="
152
152
  - !ruby/object:Gem::Version
153
153
  version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: minitest-stub-const
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
154
168
  - !ruby/object:Gem::Dependency
155
169
  name: activerecord
156
170
  requirement: !ruby/object:Gem::Requirement
@@ -498,6 +512,7 @@ files:
498
512
  - lib/dynflow/persistence_adapters/sequel_migrations/017_add_delayed_plan_frozen.rb
499
513
  - lib/dynflow/persistence_adapters/sequel_migrations/018_add_uuid_column.rb
500
514
  - lib/dynflow/persistence_adapters/sequel_migrations/019_update_mysql_time_precision.rb
515
+ - lib/dynflow/persistence_adapters/sequel_migrations/020_drop_duplicate_indices.rb
501
516
  - lib/dynflow/rails.rb
502
517
  - lib/dynflow/rails/configuration.rb
503
518
  - lib/dynflow/rails/daemon.rb
@@ -566,6 +581,7 @@ files:
566
581
  - test/middleware_test.rb
567
582
  - test/persistence_test.rb
568
583
  - test/prepare_travis_env.sh
584
+ - test/redis_locking_test.rb
569
585
  - test/rescue_test.rb
570
586
  - test/round_robin_test.rb
571
587
  - test/semaphores_test.rb
@@ -648,6 +664,7 @@ test_files:
648
664
  - test/middleware_test.rb
649
665
  - test/persistence_test.rb
650
666
  - test/prepare_travis_env.sh
667
+ - test/redis_locking_test.rb
651
668
  - test/rescue_test.rb
652
669
  - test/round_robin_test.rb
653
670
  - test/semaphores_test.rb