business_flow 0.18.1 → 0.19.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: 9163d03b65bfd1a9419b31593b1e7efca00c7eb1b13838dab9a5c402597e14e1
4
- data.tar.gz: 65cb6f0168df3fb353c70a7db3614950be0fce5304f99529357e9ccdf664d1e7
3
+ metadata.gz: 358d1c9f044cb27c403f3f09dca2ff62306a43a86b89cbdbd0a5bd2f1052384b
4
+ data.tar.gz: 9ace3fc7d47f1f383dbe5a5dcee90891b790bc5dfe602530c3230f5ae2b11db9
5
5
  SHA512:
6
- metadata.gz: 156666c6666f24bd1d4bfa024d63b337c33cec5055cb7918bde71428e452609d37b45da7f8dd74d7aafd1890952006ca89b17eeaf508fe7d70f14bb408d71522
7
- data.tar.gz: a907affc0aea2953272d33cb836b42ab8b6729a4a9ce43eaf88617ea03aae510efff9dfeb30f61198dd18a16d61ae88698d873a95223404c763d717be50ad9f3
6
+ metadata.gz: 12300927b4b0ed87c5961d86709d030059fb3734c54680b17c450a1e3f64d6e28fe2c22c158e9578f24ce137c0a24952bc3c14a501c24332fb7baf292ffedff8
7
+ data.tar.gz: b53b551074f8edb4fddc50791e1f6a64682205f49af85b1cbf6915f57677b9ab6f762c4c1e100eee658477f78b9c9abfc7199b5594490524144bf0653e5825a5
data/Gemfile.lock CHANGED
@@ -1,53 +1,52 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- business_flow (0.18.1)
4
+ business_flow (0.19.0)
5
5
  activemodel (>= 4.2, < 8)
6
6
  activesupport (>= 4.2, < 8)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activemodel (7.0.2)
12
- activesupport (= 7.0.2)
13
- activesupport (7.0.2)
11
+ activemodel (7.0.4)
12
+ activesupport (= 7.0.4)
13
+ activesupport (7.0.4)
14
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
15
15
  i18n (>= 1.6, < 2)
16
16
  minitest (>= 5.1)
17
17
  tzinfo (~> 2.0)
18
18
  ast (2.4.2)
19
- codeclimate-engine-rb (0.4.2)
20
- concurrent-ruby (1.1.9)
19
+ concurrent-ruby (1.1.10)
21
20
  diff-lcs (1.5.0)
22
- docile (1.1.5)
23
- i18n (1.9.1)
21
+ docile (1.4.0)
22
+ i18n (1.12.0)
24
23
  concurrent-ruby (~> 1.0)
25
24
  jaro_winkler (1.5.4)
26
- json (2.6.1)
27
- minitest (5.15.0)
28
- parallel (1.21.0)
29
- parser (2.5.3.0)
30
- ast (~> 2.4.0)
25
+ kwalify (0.7.2)
26
+ minitest (5.16.3)
27
+ parallel (1.22.1)
28
+ parser (3.2.2.1)
29
+ ast (~> 2.4.1)
31
30
  rainbow (3.1.1)
32
31
  rake (10.5.0)
33
- reek (4.8.2)
34
- codeclimate-engine-rb (~> 0.4.0)
35
- parser (>= 2.5.0.0, < 2.6)
32
+ reek (6.1.4)
33
+ kwalify (~> 0.7.0)
34
+ parser (~> 3.2.0)
36
35
  rainbow (>= 2.0, < 4.0)
37
36
  retryable (3.0.5)
38
- rspec (3.10.0)
39
- rspec-core (~> 3.10.0)
40
- rspec-expectations (~> 3.10.0)
41
- rspec-mocks (~> 3.10.0)
42
- rspec-core (3.10.2)
43
- rspec-support (~> 3.10.0)
44
- rspec-expectations (3.10.2)
37
+ rspec (3.12.0)
38
+ rspec-core (~> 3.12.0)
39
+ rspec-expectations (~> 3.12.0)
40
+ rspec-mocks (~> 3.12.0)
41
+ rspec-core (3.12.0)
42
+ rspec-support (~> 3.12.0)
43
+ rspec-expectations (3.12.0)
45
44
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.10.0)
47
- rspec-mocks (3.10.3)
45
+ rspec-support (~> 3.12.0)
46
+ rspec-mocks (3.12.0)
48
47
  diff-lcs (>= 1.2.0, < 2.0)
49
- rspec-support (~> 3.10.0)
50
- rspec-support (3.10.3)
48
+ rspec-support (~> 3.12.0)
49
+ rspec-support (3.12.0)
51
50
  rubocop (0.68.1)
52
51
  jaro_winkler (~> 1.5.1)
53
52
  parallel (~> 1.10)
@@ -58,13 +57,14 @@ GEM
58
57
  rubocop-rspec (1.24.0)
59
58
  rubocop (>= 0.53.0)
60
59
  ruby-progressbar (1.11.0)
61
- simplecov (0.15.1)
62
- docile (~> 1.1.0)
63
- json (>= 1.8, < 3)
64
- simplecov-html (~> 0.10.0)
65
- simplecov-html (0.10.2)
66
- timecop (0.9.4)
67
- tzinfo (2.0.4)
60
+ simplecov (0.22.0)
61
+ docile (~> 1.1)
62
+ simplecov-html (~> 0.11)
63
+ simplecov_json_formatter (~> 0.1)
64
+ simplecov-html (0.12.3)
65
+ simplecov_json_formatter (0.1.4)
66
+ timecop (0.9.6)
67
+ tzinfo (2.0.5)
68
68
  concurrent-ruby (~> 1.0)
69
69
  unicode-display_width (1.5.0)
70
70
 
@@ -74,12 +74,12 @@ PLATFORMS
74
74
  DEPENDENCIES
75
75
  business_flow!
76
76
  rake (~> 10.0)
77
- reek (~> 4.8)
77
+ reek (~> 6.1)
78
78
  retryable (~> 3.0.4)
79
79
  rspec (~> 3.0)
80
80
  rubocop (~> 0.53)
81
81
  rubocop-rspec (~> 1.24.0)
82
- simplecov (~> 0.15.1)
82
+ simplecov (~> 0.22.0)
83
83
  timecop (~> 0.9.1)
84
84
 
85
85
  BUNDLED WITH
@@ -24,11 +24,11 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'activesupport', '>= 4.2', '< 8'
25
25
 
26
26
  spec.add_development_dependency 'rake', '~> 10.0'
27
- spec.add_development_dependency 'reek', '~> 4.8'
27
+ spec.add_development_dependency 'reek', '~> 6.1'
28
28
  spec.add_development_dependency 'retryable', '~> 3.0.4'
29
29
  spec.add_development_dependency 'rspec', '~> 3.0'
30
30
  spec.add_development_dependency 'rubocop', '~> 0.53'
31
31
  spec.add_development_dependency 'rubocop-rspec', '~> 1.24.0'
32
- spec.add_development_dependency 'simplecov', '~> 0.15.1'
32
+ spec.add_development_dependency 'simplecov', '~> 0.22.0'
33
33
  spec.add_development_dependency 'timecop', '~> 0.9.1'
34
34
  end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BusinessFlow
4
+ # Mixin for business flow to acquire and retain a cluster lock.
5
+ module ClusterLock
6
+ def self.included(klass)
7
+ klass.extend(ClassMethods)
8
+ end
9
+
10
+ def self.disable!
11
+ @disabled = true
12
+ end
13
+
14
+ def self.enable!
15
+ @disabled = false
16
+ end
17
+
18
+ def self.disabled?
19
+ !!@disabled
20
+ end
21
+
22
+ def self.default_servers=(servers)
23
+ if servers.is_a?(String)
24
+ @default_servers = proc { servers }
25
+ elsif servers
26
+ @default_servers = Callable.new(servers)
27
+ else
28
+ @default_servers = nil
29
+ end
30
+ end
31
+
32
+ def self.default_servers
33
+ @default_servers ||= proc { nil }
34
+ end
35
+
36
+ def assert_cluster_lock!
37
+ @_business_flow_cluster_lock.assert! if !BusinessFlow::ClusterLock.disabled?
38
+ rescue ZK::Exceptions::ZKError => exc
39
+ errors.add(:cluster_lock, :assert_failed, message: exc.message)
40
+ raise
41
+ end
42
+
43
+ # DSL Methods
44
+ module ClassMethods
45
+ # Error raised when there is an internal issue with acquiring a lock.
46
+ class LockFailure < StandardError
47
+ attr_reader :error_type
48
+ def initialize(error_type, message)
49
+ @error_type = error_type
50
+ super(message)
51
+ end
52
+
53
+ def add_to(flow)
54
+ errors = flow.errors
55
+ errors.add(:cluster_lock, error_type, message: message) if !errors.key?(:cluster_lock)
56
+ flow
57
+ end
58
+ end
59
+
60
+ # Holder for information about the lock
61
+ class LockInfo
62
+ attr_reader :lock_name, :zookeeper_servers
63
+
64
+ def initialize(lock_name, zookeeper_servers)
65
+ @lock_name = lock_name
66
+ @zookeeper_servers = zookeeper_servers
67
+ end
68
+ end
69
+
70
+ def with_cluster_lock(lock_name = nil, opts = {}, &blk)
71
+ if lock_name.is_a?(String)
72
+ @lock_name = Step.new(Callable.new(proc { lock_name }), {})
73
+ elsif lock_name || blk
74
+ @lock_name = Step.new(Callable.new(lock_name || blk),
75
+ { default_output: :lock_name }.merge(opts))
76
+ else
77
+ @lock_name ||= Step.new(Callable.new(default_lock_name), opts)
78
+ end
79
+ end
80
+
81
+ def with_zookeeper_servers(servers = nil, opts = {}, &blk)
82
+ if servers.is_a?(String)
83
+ @zookeeper_servers = Step.new(Callable.new(proc { servers }), {})
84
+ elsif servers || blk
85
+ @zookeeper_servers = Step.new(Callable.new(servers || blk),
86
+ { default_output: :zookeeper_servers }.merge(opts))
87
+ else
88
+ @zookeeper_servers || Step.new(BusinessFlow::ClusterLock.default_servers, opts)
89
+ end
90
+ end
91
+
92
+ def default_lock_name
93
+ proc { self.class.name }
94
+ end
95
+
96
+ def build(parameter_object)
97
+ add_cluster_luck_info_to_result_class
98
+ super(parameter_object)
99
+ end
100
+
101
+ def execute(flow)
102
+ lock_info = LockInfo.new(
103
+ ClassMethods.lock_name(flow),
104
+ ClassMethods.zookeeper_server_list(flow)
105
+ )
106
+ ClassMethods.with_lock(flow, lock_info) do
107
+ super(flow)._business_flow_cluster_lock_finalize(lock_info)
108
+ end
109
+ rescue LockFailure => exc
110
+ return result_from(exc.add_to(flow))._business_flow_cluster_lock_finalize(lock_info)
111
+ end
112
+
113
+ RESULT_FINALIZE = proc do |cluster_lock_info|
114
+ @cluster_lock_info = cluster_lock_info
115
+ self
116
+ end
117
+
118
+ def add_cluster_luck_info_to_result_class
119
+ return if @cluster_lock_info_added
120
+ result_class = const_get(:Result)
121
+ DSL::PublicField.new(:cluster_lock_info).add_to(result_class)
122
+ result_class.send(:define_method, :_business_flow_cluster_lock_finalize,
123
+ RESULT_FINALIZE)
124
+ @cluster_lock_info_added = true
125
+ end
126
+
127
+ # :reek:NilCheck
128
+ def self.zookeeper_server_list(flow)
129
+ servers = catch(:halt_step) { flow.class.with_zookeeper_servers.call(flow)&.merge_into(flow)&.to_s }
130
+ if servers.nil? || servers.length == 0
131
+ raise LockFailure.new(:no_servers, 'no zookeeper servers provided')
132
+ end
133
+ servers
134
+ end
135
+
136
+ # :reek:NilCheck
137
+ def self.lock_name(flow)
138
+ lock_name = catch(:halt_step) { flow.class.with_cluster_lock.call(flow)&.merge_into(flow)&.to_s }
139
+ if lock_name.nil? || lock_name.length == 0
140
+ raise LockFailure.new(:no_lock_name, 'no lock name provided')
141
+ end
142
+ lock_name
143
+ end
144
+
145
+ def self.instrumented_acquire_lock(flow, lock_info)
146
+ flow.class.instrument(:cluster_lock_setup, flow) do |payload|
147
+ payload[:lock_name] = lock_info.lock_name if payload
148
+ acquire_lock(flow, lock_info, payload)
149
+ end
150
+ end
151
+
152
+ def self.acquire_lock(flow, lock_info, payload)
153
+ zk_connection = ZK::Client::Threaded.new(lock_info.zookeeper_servers)
154
+ lock = flow.instance_variable_set(
155
+ :@_business_flow_cluster_lock,
156
+ ZK::Locker::ExclusiveLocker.new(zk_connection, lock_info.lock_name)
157
+ )
158
+ inner_acquire_lock(zk_connection, lock, payload)
159
+ end
160
+
161
+ def self.inner_acquire_lock(zk_connection, lock, payload)
162
+ lock_held = lock.lock(wait: false)
163
+ payload[:lock_acquired] = lock_held if payload
164
+ if !lock_held
165
+ zk_connection.close!
166
+ raise LockFailure.new(:lock_unavailable, 'the lock was not available')
167
+ end
168
+ [zk_connection, lock]
169
+ end
170
+
171
+ def self.cleanup(lock, zk_connection)
172
+ lock.unlock if lock
173
+ zk_connection.close! if zk_connection
174
+ end
175
+
176
+ def self.with_lock(flow, lock_info, &blk)
177
+ zk_connection, lock =
178
+ if !BusinessFlow::ClusterLock.disabled?
179
+ instrumented_acquire_lock(flow, lock_info)
180
+ end
181
+ yield lock_info
182
+ rescue ZK::Exceptions::LockAssertionFailedError => exc
183
+ # This would occur if we asserted a cluster lock while executing the flow.
184
+ # This will have set an error on the flow, so we can carry on.
185
+ raise LockFailure.new(:assert_failed, exc.message)
186
+ rescue ZK::Exceptions::OperationTimeOut
187
+ # Sometimes this happens. Just let the ensure block take care of everything
188
+ ensure
189
+ cleanup(lock, zk_connection)
190
+ end
191
+ end
192
+ end
193
+ end
@@ -14,7 +14,7 @@ module BusinessFlow
14
14
 
15
15
  if !ActiveModel::Errors.instance_methods.include?(:merge!)
16
16
  # ActiveModel 5 added details (which we do not use here) and #merge!
17
- # :reek:PrimaDonnaMethod Look it's the API.
17
+ # :reek:MissingSafeMethod Look it's the API.
18
18
  class ::ActiveModel::Errors
19
19
  def merge!(other)
20
20
  other.each do |attribute, message|
@@ -103,7 +103,9 @@ module BusinessFlow
103
103
  end
104
104
 
105
105
  def call(parameter_object = {})
106
- execute(build(parameter_object))
106
+ flow = build(parameter_object)
107
+ return result_from(flow) if flow.errors?
108
+ execute(flow)
107
109
  end
108
110
 
109
111
  # :reek:UtilityFunction This is a function on us so that other modules
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BusinessFlow
4
- VERSION = '0.18.1'.freeze
4
+ VERSION = '0.19.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: business_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.1
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Scarborough
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-11-01 00:00:00.000000000 Z
11
+ date: 2023-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -70,14 +70,14 @@ dependencies:
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '4.8'
73
+ version: '6.1'
74
74
  type: :development
75
75
  prerelease: false
76
76
  version_requirements: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '4.8'
80
+ version: '6.1'
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: retryable
83
83
  requirement: !ruby/object:Gem::Requirement
@@ -140,14 +140,14 @@ dependencies:
140
140
  requirements:
141
141
  - - "~>"
142
142
  - !ruby/object:Gem::Version
143
- version: 0.15.1
143
+ version: 0.22.0
144
144
  type: :development
145
145
  prerelease: false
146
146
  version_requirements: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - "~>"
149
149
  - !ruby/object:Gem::Version
150
- version: 0.15.1
150
+ version: 0.22.0
151
151
  - !ruby/object:Gem::Dependency
152
152
  name: timecop
153
153
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +184,7 @@ files:
184
184
  - lib/business_flow/base.rb
185
185
  - lib/business_flow/cacheable.rb
186
186
  - lib/business_flow/callable.rb
187
+ - lib/business_flow/cluster_lock.rb
187
188
  - lib/business_flow/compat.rb
188
189
  - lib/business_flow/default_step_executor.rb
189
190
  - lib/business_flow/dsl.rb