ruby_event_store-outbox 0.0.15 → 0.0.16

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: a4c373da7338563c51f8bc7d3bae7e56ceb99f9b75d8104196d003f5316cdb6a
4
- data.tar.gz: fbb6dad9bdaee2fea028a2cea8541eb6a9f964e1d10d4e704ebe3f715a88d623
3
+ metadata.gz: '008b96c4b43e3cbfc23d0aa8c9ebb160f0adbfcfc82a976967f1c56338346cc0'
4
+ data.tar.gz: e65157c0638ecec0a4967a251e820979d18eb92e55212f107be95c23d5a4315b
5
5
  SHA512:
6
- metadata.gz: e6f7ac3498d139a98ef14b8cbb7abe7bd4af8b794b41f90c6b65150523410a25a59ff6e863301d87f3a90bb5be80cc422d7ae94dd8a16fedd3b88a1db334eee0
7
- data.tar.gz: 1c30a1e0c60bee39d1064e0aa7c535416a3cca7d4aa25328ab684741bf4a6bc65373e412388e65d5e57183a4dbf25e4fff385c456cd562c1afc2fe9e04e1d5d8
6
+ metadata.gz: f0181744b8c621bc1f0b20d6d4a65ac779db7ba48a80c9f1a02f39208762aa82e5579e745f20cd35f2b54992876065f803e04e24c1c239abe22641f927327070
7
+ data.tar.gz: 453ce565584436b1b7dd145c738a0d50e95821cb8b8f5d9c00f0d70d2a8ea88e585402de534388459d04853d293c0bcbf7027285d31bbd00aeaef09471cf666d
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # Ruby Event Store Outbox
2
2
 
3
- Very much work in progress.
3
+ ![Ruby Event Store Outbox](https://github.com/RailsEventStore/rails_event_store/workflows/ruby_event_store-outbox/badge.svg)
4
4
 
5
+ **Experimental feature of RES ecosystem.**
6
+
7
+ This repository includes a process and a Rails Event Store scheduler, which can be used to transactionally enqueue background jobs into your background jobs tool of choice. The scheduler included in this repo adds the jobs into the RDBMS into specific table instead of redis inside your transaction, and the process is enqueuing the jobs from that table to the background jobs tool.
5
8
 
6
9
  ## Installation (app)
7
10
 
@@ -35,7 +38,21 @@ end
35
38
  Run following process in any way you prefer:
36
39
 
37
40
  ```
38
- res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" --redis-url="redis://localhost:6379/0" --log-level=info
41
+ res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" --redis-url="redis://localhost:6379/0" --log-level=info --split-keys=sidekiq_queue1,sidekiq_queue2
42
+ ```
43
+
44
+ It is possible to run as many instances as you prefer, but it does not make sense to run more instances than there are different split keys (sidekiq queues), as one process is operating at one moment only one split key.
45
+
46
+ ### Metrics
47
+
48
+ It is possible for the outbox process to send metrics to InfluxDB. In order to do that, specify a `--metrics-url` parameter, for example:
49
+
50
+ ```
51
+ res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" \
52
+ --redis-url="redis://localhost:6379/0" \
53
+ --log-level=info \
54
+ --split-keys=sidekiq_queue1,sidekiq_queue2 \
55
+ --metrics-url=http://user:password@localhost:8086/dbname"
39
56
  ```
40
57
 
41
58
 
@@ -16,12 +16,7 @@ module RubyEventStore
16
16
 
17
17
  private
18
18
 
19
- def rails_version
20
- Rails::VERSION::STRING
21
- end
22
-
23
19
  def migration_version
24
- return nil if Gem::Version.new(rails_version) < Gem::Version.new("5.0.0")
25
20
  "[4.2]"
26
21
  end
27
22
 
@@ -6,7 +6,7 @@ module RubyEventStore
6
6
  end
7
7
 
8
8
  require_relative 'outbox/fetch_specification'
9
- require_relative 'outbox/record'
9
+ require_relative 'outbox/repository'
10
10
  require_relative 'outbox/sidekiq_scheduler'
11
11
  require_relative 'outbox/consumer'
12
12
  require_relative 'outbox/version'
@@ -0,0 +1,19 @@
1
+ module RubyEventStore
2
+ module Outbox
3
+ module CleanupStrategies
4
+ class CleanOldEnqueued
5
+ def initialize(repository, duration)
6
+ @repository = repository
7
+ @duration = duration
8
+ end
9
+
10
+ def call(fetch_specification)
11
+ repository.delete_enqueued_older_than(fetch_specification, duration)
12
+ end
13
+
14
+ private
15
+ attr_reader :repository, :duration
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module RubyEventStore
2
+ module Outbox
3
+ module CleanupStrategies
4
+ class None
5
+ def initialize
6
+ end
7
+
8
+ def call(_fetch_specification)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -6,11 +6,11 @@ require "ruby_event_store/outbox/metrics"
6
6
  module RubyEventStore
7
7
  module Outbox
8
8
  class CLI
9
- Options = Struct.new(:database_url, :redis_url, :log_level, :split_keys, :message_format, :batch_size, :metrics_url)
9
+ Options = Struct.new(:database_url, :redis_url, :log_level, :split_keys, :message_format, :batch_size, :metrics_url, :cleanup_strategy)
10
10
 
11
11
  class Parser
12
12
  def self.parse(argv)
13
- options = Options.new(nil, nil, :warn, nil, nil, 100)
13
+ options = Options.new(nil, nil, :warn, nil, nil, 100, nil, :none)
14
14
  OptionParser.new do |option_parser|
15
15
  option_parser.banner = "Usage: res_outbox [options]"
16
16
 
@@ -42,6 +42,10 @@ module RubyEventStore
42
42
  options.metrics_url = metrics_url
43
43
  end
44
44
 
45
+ option_parser.on("--cleanup CLEANUP_STRATEGY", "A strategy for cleaning old records. One of: none or iso8601 duration format how old enqueued records should be removed") do |cleanup_strategy|
46
+ options.cleanup_strategy = cleanup_strategy
47
+ end
48
+
45
49
  option_parser.on_tail("--version", "Show version") do
46
50
  puts VERSION
47
51
  exit
@@ -67,11 +71,12 @@ module RubyEventStore
67
71
  batch_size: options.batch_size,
68
72
  database_url: options.database_url,
69
73
  redis_url: options.redis_url,
74
+ cleanup: options.cleanup_strategy,
70
75
  )
71
76
  metrics = Metrics.from_url(options.metrics_url)
72
77
  outbox_consumer = RubyEventStore::Outbox::Consumer.new(
73
78
  consumer_uuid,
74
- options,
79
+ consumer_configuration,
75
80
  logger: logger,
76
81
  metrics: metrics,
77
82
  )
@@ -1,10 +1,12 @@
1
1
  require "logger"
2
2
  require "redis"
3
3
  require "active_record"
4
- require "ruby_event_store/outbox/record"
4
+ require "ruby_event_store/outbox/repository"
5
5
  require "ruby_event_store/outbox/sidekiq5_format"
6
6
  require "ruby_event_store/outbox/sidekiq_processor"
7
7
  require "ruby_event_store/outbox/fetch_specification"
8
+ require "ruby_event_store/outbox/cleanup_strategies/none"
9
+ require "ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued"
8
10
 
9
11
  module RubyEventStore
10
12
  module Outbox
@@ -18,13 +20,15 @@ module RubyEventStore
18
20
  message_format:,
19
21
  batch_size:,
20
22
  database_url:,
21
- redis_url:
23
+ redis_url:,
24
+ cleanup:
22
25
  )
23
26
  @split_keys = split_keys
24
27
  @message_format = message_format
25
28
  @batch_size = batch_size || 100
26
29
  @database_url = database_url
27
30
  @redis_url = redis_url
31
+ @cleanup = cleanup
28
32
  freeze
29
33
  end
30
34
 
@@ -35,10 +39,11 @@ module RubyEventStore
35
39
  batch_size: overriden_options.fetch(:batch_size, batch_size),
36
40
  database_url: overriden_options.fetch(:database_url, database_url),
37
41
  redis_url: overriden_options.fetch(:redis_url, redis_url),
42
+ cleanup: overriden_options.fetch(:cleanup, cleanup)
38
43
  )
39
44
  end
40
45
 
41
- attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url
46
+ attr_reader :split_keys, :message_format, :batch_size, :database_url, :redis_url, :cleanup
42
47
  end
43
48
 
44
49
  def initialize(consumer_uuid, configuration, clock: Time, logger:, metrics:)
@@ -48,16 +53,20 @@ module RubyEventStore
48
53
  @metrics = metrics
49
54
  @batch_size = configuration.batch_size
50
55
  @consumer_uuid = consumer_uuid
51
- ActiveRecord::Base.establish_connection(configuration.database_url) unless ActiveRecord::Base.connected?
52
- if ActiveRecord::Base.connection.adapter_name == "Mysql2"
53
- ActiveRecord::Base.connection.execute("SET SESSION innodb_lock_wait_timeout = 1;")
54
- end
55
56
 
56
57
  raise "Unknown format" if configuration.message_format != SIDEKIQ5_FORMAT
57
58
  @processor = SidekiqProcessor.new(Redis.new(url: configuration.redis_url))
58
59
 
59
60
  @gracefully_shutting_down = false
60
61
  prepare_traps
62
+
63
+ @repository = Repository.new(configuration.database_url)
64
+ @cleanup_strategy = case configuration.cleanup
65
+ when :none
66
+ CleanupStrategies::None.new
67
+ else
68
+ CleanupStrategies::CleanOldEnqueued.new(repository, ActiveSupport::Duration.parse(configuration.cleanup))
69
+ end
61
70
  end
62
71
 
63
72
  def init
@@ -105,7 +114,7 @@ module RubyEventStore
105
114
  now = @clock.now.utc
106
115
  processor.process(record, now)
107
116
 
108
- record.update_column(:enqueued_at, now)
117
+ repository.mark_as_enqueued(record, now)
109
118
  something_processed |= true
110
119
  updated_record_ids << record.id
111
120
  rescue => e
@@ -124,8 +133,8 @@ module RubyEventStore
124
133
 
125
134
  logger.info "Sent #{updated_record_ids.size} messages from outbox table"
126
135
 
127
- obtained_lock = refresh_lock_for_process(obtained_lock)
128
- break unless obtained_lock
136
+ refresh_successful = refresh_lock_for_process(obtained_lock)
137
+ break unless refresh_successful
129
138
  end
130
139
 
131
140
  metrics.write_point_queue(
@@ -136,16 +145,18 @@ module RubyEventStore
136
145
 
137
146
  release_lock_for_process(fetch_specification)
138
147
 
148
+ cleanup_strategy.call(fetch_specification)
149
+
139
150
  processor.after_batch
140
151
 
141
152
  something_processed
142
153
  end
143
154
 
144
155
  private
145
- attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid
156
+ attr_reader :split_keys, :logger, :batch_size, :metrics, :processor, :consumer_uuid, :repository, :cleanup_strategy
146
157
 
147
158
  def obtain_lock_for_process(fetch_specification)
148
- result = Lock.obtain(fetch_specification, consumer_uuid, clock: @clock)
159
+ result = repository.obtain_lock_for_process(fetch_specification, consumer_uuid, clock: @clock)
149
160
  case result
150
161
  when :deadlocked
151
162
  logger.warn "Obtaining lock for split_key '#{fetch_specification.split_key}' failed (deadlock)"
@@ -165,7 +176,7 @@ module RubyEventStore
165
176
  end
166
177
 
167
178
  def release_lock_for_process(fetch_specification)
168
- result = Lock.release(fetch_specification, consumer_uuid)
179
+ result = repository.release_lock_for_process(fetch_specification, consumer_uuid)
169
180
  case result
170
181
  when :ok
171
182
  when :deadlocked
@@ -185,6 +196,8 @@ module RubyEventStore
185
196
  def refresh_lock_for_process(lock)
186
197
  result = lock.refresh(clock: @clock)
187
198
  case result
199
+ when :ok
200
+ return true
188
201
  when :deadlocked
189
202
  logger.warn "Refreshing lock for split_key '#{lock.split_key}' failed (deadlock)"
190
203
  metrics.write_operation_result("refresh", "deadlocked")
@@ -198,7 +211,7 @@ module RubyEventStore
198
211
  metrics.write_operation_result("refresh", "stolen")
199
212
  return false
200
213
  else
201
- return result
214
+ raise "Unexpected result #{result}"
202
215
  end
203
216
  end
204
217
 
@@ -216,11 +229,11 @@ module RubyEventStore
216
229
  end
217
230
 
218
231
  def retrieve_batch(fetch_specification)
219
- Record.remaining_for(fetch_specification).order("id ASC").limit(batch_size).to_a
232
+ repository.retrieve_batch(fetch_specification, batch_size)
220
233
  end
221
234
 
222
235
  def get_remaining_count(fetch_specification)
223
- Record.remaining_for(fetch_specification).count
236
+ repository.get_remaining_count(fetch_specification)
224
237
  end
225
238
  end
226
239
  end
@@ -8,6 +8,24 @@ module RubyEventStore
8
8
  end
9
9
 
10
10
  attr_reader :message_format, :split_key
11
+
12
+ def ==(other)
13
+ other.instance_of?(self.class) &&
14
+ other.message_format.eql?(message_format) &&
15
+ other.split_key.eql?(split_key)
16
+ end
17
+
18
+ BIG_VALUE = 0b111111100100010000010010110010101011011101110101001100100110000
19
+
20
+ def hash
21
+ [
22
+ self.class,
23
+ message_format,
24
+ split_key,
25
+ ].hash ^ BIG_VALUE
26
+ end
27
+
28
+ alias_method :eql?, :==
11
29
  end
12
30
  end
13
31
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'active_support/core_ext/numeric/time.rb'
5
+
6
+ module RubyEventStore
7
+ module Outbox
8
+ class Repository
9
+ RECENTLY_LOCKED_DURATION = 10.minutes
10
+
11
+ class Record < ::ActiveRecord::Base
12
+ self.primary_key = :id
13
+ self.table_name = 'event_store_outbox'
14
+
15
+ def self.remaining_for(fetch_specification)
16
+ where(format: fetch_specification.message_format, split_key: fetch_specification.split_key, enqueued_at: nil)
17
+ end
18
+
19
+ def self.for_fetch_specification(fetch_specification)
20
+ where(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
21
+ end
22
+
23
+ def hash_payload
24
+ JSON.parse(payload).deep_symbolize_keys
25
+ end
26
+
27
+ def enqueued?
28
+ !enqueued_at.nil?
29
+ end
30
+ end
31
+
32
+ class Lock < ::ActiveRecord::Base
33
+ self.table_name = 'event_store_outbox_locks'
34
+
35
+ def self.obtain(fetch_specification, process_uuid, clock:)
36
+ transaction do
37
+ l = get_lock_record(fetch_specification)
38
+
39
+ if l.recently_locked?
40
+ :taken
41
+ else
42
+ l.update!(
43
+ locked_by: process_uuid,
44
+ locked_at: clock.now,
45
+ )
46
+ l
47
+ end
48
+ end
49
+ rescue ActiveRecord::Deadlocked
50
+ :deadlocked
51
+ rescue ActiveRecord::LockWaitTimeout
52
+ :lock_timeout
53
+ end
54
+
55
+ def refresh(clock:)
56
+ transaction do
57
+ current_process_uuid = locked_by
58
+ lock!
59
+ if locked_by == current_process_uuid
60
+ update!(locked_at: clock.now)
61
+ :ok
62
+ else
63
+ :stolen
64
+ end
65
+ end
66
+ rescue ActiveRecord::Deadlocked
67
+ :deadlocked
68
+ rescue ActiveRecord::LockWaitTimeout
69
+ :lock_timeout
70
+ end
71
+
72
+ def self.release(fetch_specification, process_uuid)
73
+ transaction do
74
+ l = get_lock_record(fetch_specification)
75
+ if !l.locked_by?(process_uuid)
76
+ :not_taken_by_this_process
77
+ else
78
+ l.update!(locked_by: nil, locked_at: nil)
79
+ :ok
80
+ end
81
+ end
82
+ rescue ActiveRecord::Deadlocked
83
+ :deadlocked
84
+ rescue ActiveRecord::LockWaitTimeout
85
+ :lock_timeout
86
+ end
87
+
88
+ def locked_by?(process_uuid)
89
+ locked_by.eql?(process_uuid)
90
+ end
91
+
92
+ def recently_locked?
93
+ locked_by && locked_at > RECENTLY_LOCKED_DURATION.ago
94
+ end
95
+
96
+ def fetch_specification
97
+ FetchSpecification.new(format, split_key)
98
+ end
99
+
100
+ private
101
+ def self.lock_for_split_key(fetch_specification)
102
+ lock.find_by(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
103
+ end
104
+
105
+ def self.get_lock_record(fetch_specification)
106
+ l = lock_for_split_key(fetch_specification)
107
+ if l.nil?
108
+ begin
109
+ l = create!(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
110
+ rescue ActiveRecord::RecordNotUnique
111
+ l = lock_for_split_key(fetch_specification)
112
+ end
113
+ end
114
+ l
115
+ end
116
+ end
117
+
118
+ def initialize(database_url)
119
+ ActiveRecord::Base.establish_connection(database_url) unless ActiveRecord::Base.connected?
120
+ if ActiveRecord::Base.connection.adapter_name == "Mysql2"
121
+ ActiveRecord::Base.connection.execute("SET SESSION innodb_lock_wait_timeout = 1;")
122
+ end
123
+ end
124
+
125
+ def retrieve_batch(fetch_specification, batch_size)
126
+ Record.remaining_for(fetch_specification).order("id ASC").limit(batch_size).to_a
127
+ end
128
+
129
+ def get_remaining_count(fetch_specification)
130
+ Record.remaining_for(fetch_specification).count
131
+ end
132
+
133
+ def obtain_lock_for_process(fetch_specification, process_uuid, clock:)
134
+ Lock.obtain(fetch_specification, process_uuid, clock: clock)
135
+ end
136
+
137
+ def release_lock_for_process(fetch_specification, process_uuid)
138
+ Lock.release(fetch_specification, process_uuid)
139
+ end
140
+
141
+ def mark_as_enqueued(record, now)
142
+ record.update_column(:enqueued_at, now)
143
+ end
144
+
145
+ def delete_enqueued_older_than(fetch_specification, duration)
146
+ Record
147
+ .for_fetch_specification(fetch_specification)
148
+ .where("enqueued_at < ?", duration.ago)
149
+ .delete_all
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sidekiq'
4
- require "ruby_event_store/outbox/sidekiq5_format"
4
+ require "ruby_event_store/outbox/repository"
5
5
 
6
6
  module RubyEventStore
7
7
  module Outbox
@@ -15,13 +15,16 @@ module RubyEventStore
15
15
  normalized_item = sidekiq_client.__send__(:normalize_item, item)
16
16
  payload = sidekiq_client.__send__(:process_single, normalized_item.fetch('class'), normalized_item)
17
17
  if payload
18
- Record.create!(
18
+ Repository::Record.create!(
19
19
  format: SIDEKIQ5_FORMAT,
20
20
  split_key: payload.fetch('queue'),
21
21
  payload: payload.to_json
22
22
  )
23
23
  end
24
24
  end
25
+
26
+ private
27
+ attr_reader :repository
25
28
  end
26
29
  end
27
30
  end
@@ -9,8 +9,8 @@ module RubyEventStore
9
9
  @sidekiq_producer = SidekiqProducer.new
10
10
  end
11
11
 
12
- def call(klass, serialized_event)
13
- sidekiq_producer.call(klass, [serialized_event.to_h])
12
+ def call(klass, serialized_record)
13
+ sidekiq_producer.call(klass, [serialized_record.to_h])
14
14
  end
15
15
 
16
16
  def verify(subscriber)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyEventStore
4
4
  module Outbox
5
- VERSION = "0.0.15"
5
+ VERSION = "0.0.16"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_event_store-outbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arkency
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-30 00:00:00.000000000 Z
11
+ date: 2021-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_event_store
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '3.0'
33
+ version: '5.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '3.0'
40
+ version: '5.2'
41
41
  description:
42
42
  email:
43
43
  - dev@arkency.com
@@ -51,6 +51,8 @@ files:
51
51
  - lib/generators/ruby_event_store/outbox/migration_generator.rb
52
52
  - lib/generators/ruby_event_store/outbox/templates/create_event_store_outbox_template.rb
53
53
  - lib/ruby_event_store/outbox.rb
54
+ - lib/ruby_event_store/outbox/cleanup_strategies/clean_old_enqueued.rb
55
+ - lib/ruby_event_store/outbox/cleanup_strategies/none.rb
54
56
  - lib/ruby_event_store/outbox/cli.rb
55
57
  - lib/ruby_event_store/outbox/consumer.rb
56
58
  - lib/ruby_event_store/outbox/consumer_process.rb
@@ -58,7 +60,7 @@ files:
58
60
  - lib/ruby_event_store/outbox/metrics.rb
59
61
  - lib/ruby_event_store/outbox/metrics/influx.rb
60
62
  - lib/ruby_event_store/outbox/metrics/null.rb
61
- - lib/ruby_event_store/outbox/record.rb
63
+ - lib/ruby_event_store/outbox/repository.rb
62
64
  - lib/ruby_event_store/outbox/sidekiq5_format.rb
63
65
  - lib/ruby_event_store/outbox/sidekiq_message_handler.rb
64
66
  - lib/ruby_event_store/outbox/sidekiq_processor.rb
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_record'
4
-
5
- module RubyEventStore
6
- module Outbox
7
- class Record < ::ActiveRecord::Base
8
- self.primary_key = :id
9
- self.table_name = 'event_store_outbox'
10
-
11
- def self.remaining_for(fetch_specification)
12
- where(format: fetch_specification.message_format, split_key: fetch_specification.split_key, enqueued_at: nil)
13
- end
14
-
15
- def hash_payload
16
- JSON.parse(payload).deep_symbolize_keys
17
- end
18
-
19
- def enqueued?
20
- !enqueued_at.nil?
21
- end
22
- end
23
-
24
- class Lock < ::ActiveRecord::Base
25
- self.table_name = 'event_store_outbox_locks'
26
-
27
- def self.obtain(fetch_specification, process_uuid, clock:)
28
- l = nil
29
- transaction do
30
- l = get_lock_record(fetch_specification)
31
-
32
- return :taken if l.recently_locked?
33
-
34
- l.update!(
35
- locked_by: process_uuid,
36
- locked_at: clock.now,
37
- )
38
- end
39
- l
40
- rescue ActiveRecord::Deadlocked
41
- :deadlocked
42
- rescue ActiveRecord::LockWaitTimeout
43
- :lock_timeout
44
- end
45
-
46
- def refresh(clock:)
47
- transaction do
48
- current_process_uuid = locked_by
49
- lock!
50
- if locked_by == current_process_uuid
51
- update!(locked_at: clock.now)
52
- return self
53
- else
54
- return :stolen
55
- end
56
- end
57
- rescue ActiveRecord::Deadlocked
58
- :deadlocked
59
- rescue ActiveRecord::LockWaitTimeout
60
- :lock_timeout
61
- end
62
-
63
- def self.release(fetch_specification, process_uuid)
64
- transaction do
65
- l = get_lock_record(fetch_specification)
66
- return :not_taken_by_this_process if !l.locked_by?(process_uuid)
67
-
68
- l.update!(locked_by: nil, locked_at: nil)
69
- end
70
- :ok
71
- rescue ActiveRecord::Deadlocked
72
- :deadlocked
73
- rescue ActiveRecord::LockWaitTimeout
74
- :lock_timeout
75
- end
76
-
77
- def locked_by?(process_uuid)
78
- locked_by.eql?(process_uuid)
79
- end
80
-
81
- def recently_locked?
82
- locked_by && locked_at > 10.minutes.ago
83
- end
84
-
85
- private
86
- def self.lock_for_split_key(fetch_specification)
87
- lock.find_by(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
88
- end
89
-
90
- def self.get_lock_record(fetch_specification)
91
- l = lock_for_split_key(fetch_specification)
92
- if l.nil?
93
- begin
94
- l = create!(format: fetch_specification.message_format, split_key: fetch_specification.split_key)
95
- rescue ActiveRecord::RecordNotUnique
96
- l = lock_for_split_key(fetch_specification)
97
- end
98
- end
99
- l
100
- end
101
- end
102
- end
103
- end