sbmt-outbox 5.0.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -9
  3. data/app/interactors/sbmt/outbox/process_item.rb +2 -1
  4. data/app/interactors/sbmt/outbox/retry_strategies/base.rb +15 -0
  5. data/app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb +2 -32
  6. data/app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb +3 -5
  7. data/app/interactors/sbmt/outbox/retry_strategies/latest_available.rb +39 -0
  8. data/app/interactors/sbmt/outbox/retry_strategies/no_delay.rb +13 -0
  9. data/app/models/sbmt/outbox/base_item.rb +9 -8
  10. data/app/models/sbmt/outbox/base_item_config.rb +23 -4
  11. data/config/initializers/yabeda.rb +32 -5
  12. data/lib/generators/helpers/migration.rb +2 -2
  13. data/lib/sbmt/outbox/cli.rb +50 -7
  14. data/lib/sbmt/outbox/engine.rb +26 -0
  15. data/lib/sbmt/outbox/logger.rb +6 -0
  16. data/lib/sbmt/outbox/v1/thread_pool.rb +110 -0
  17. data/lib/sbmt/outbox/v1/throttler.rb +54 -0
  18. data/lib/sbmt/outbox/v1/worker.rb +231 -0
  19. data/lib/sbmt/outbox/v2/box_processor.rb +148 -0
  20. data/lib/sbmt/outbox/v2/poll_throttler/base.rb +43 -0
  21. data/lib/sbmt/outbox/v2/poll_throttler/composite.rb +42 -0
  22. data/lib/sbmt/outbox/v2/poll_throttler/fixed_delay.rb +28 -0
  23. data/lib/sbmt/outbox/v2/poll_throttler/noop.rb +17 -0
  24. data/lib/sbmt/outbox/v2/poll_throttler/rate_limited.rb +24 -0
  25. data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_size.rb +46 -0
  26. data/lib/sbmt/outbox/v2/poll_throttler/redis_queue_time_lag.rb +45 -0
  27. data/lib/sbmt/outbox/v2/poll_throttler.rb +49 -0
  28. data/lib/sbmt/outbox/v2/poller.rb +180 -0
  29. data/lib/sbmt/outbox/v2/processor.rb +101 -0
  30. data/lib/sbmt/outbox/v2/redis_job.rb +42 -0
  31. data/lib/sbmt/outbox/v2/tasks/base.rb +48 -0
  32. data/lib/sbmt/outbox/v2/tasks/default.rb +17 -0
  33. data/lib/sbmt/outbox/v2/tasks/poll.rb +34 -0
  34. data/lib/sbmt/outbox/v2/tasks/process.rb +31 -0
  35. data/lib/sbmt/outbox/v2/thread_pool.rb +152 -0
  36. data/lib/sbmt/outbox/v2/throttler.rb +13 -0
  37. data/lib/sbmt/outbox/v2/worker.rb +52 -0
  38. data/lib/sbmt/outbox/version.rb +1 -1
  39. data/lib/sbmt/outbox.rb +16 -2
  40. metadata +41 -5
  41. data/lib/sbmt/outbox/thread_pool.rb +0 -108
  42. data/lib/sbmt/outbox/throttler.rb +0 -52
  43. data/lib/sbmt/outbox/worker.rb +0 -233
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/poll_throttler/base"
4
+
5
+ module Sbmt
6
+ module Outbox
7
+ module V2
8
+ module PollThrottler
9
+ class RedisQueueSize < Base
10
+ delegate :redis_job_queue_size, to: "Yabeda.box_worker"
11
+
12
+ def initialize(redis:, min_size: -1, max_size: 100, delay: 0)
13
+ super()
14
+
15
+ @redis = redis
16
+ @min_size = min_size
17
+ @max_size = max_size
18
+ @delay = delay
19
+ end
20
+
21
+ def wait(worker_num, poll_task, _task_result)
22
+ # LLEN is O(1)
23
+ queue_size = @redis.call("LLEN", poll_task.redis_queue).to_i
24
+ redis_job_queue_size.set(metric_tags(poll_task), queue_size)
25
+
26
+ if queue_size < @min_size
27
+ # just throttle (not skip) to wait for job queue size becomes acceptable
28
+ sleep(@delay)
29
+ return Success(Sbmt::Outbox::V2::Throttler::THROTTLE_STATUS)
30
+ end
31
+
32
+ if queue_size > @max_size
33
+ # completely skip poll-cycle if job queue is oversized
34
+ sleep(@delay)
35
+ return Success(Sbmt::Outbox::V2::Throttler::SKIP_STATUS)
36
+ end
37
+
38
+ Success(Sbmt::Outbox::V2::Throttler::NOOP_STATUS)
39
+ rescue => ex
40
+ Failure(ex.message)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/poll_throttler/base"
4
+ require "sbmt/outbox/v2/redis_job"
5
+
6
+ module Sbmt
7
+ module Outbox
8
+ module V2
9
+ module PollThrottler
10
+ class RedisQueueTimeLag < Base
11
+ delegate :redis_job_queue_time_lag, to: "Yabeda.box_worker"
12
+
13
+ def initialize(redis:, min_lag: 5, delay: 0)
14
+ super()
15
+
16
+ @redis = redis
17
+ @min_lag = min_lag
18
+ @delay = delay
19
+ end
20
+
21
+ def wait(worker_num, poll_task, _task_result)
22
+ # LINDEX is O(1) for first/last element
23
+ oldest_job = @redis.call("LINDEX", poll_task.redis_queue, -1)
24
+ return Success(Sbmt::Outbox::V2::Throttler::NOOP_STATUS) if oldest_job.nil?
25
+
26
+ job = RedisJob.deserialize!(oldest_job)
27
+ time_lag = Time.current.to_i - job.timestamp
28
+
29
+ redis_job_queue_time_lag.set(metric_tags(poll_task), time_lag)
30
+
31
+ if time_lag <= @min_lag
32
+ sleep(@delay)
33
+ return Success(Sbmt::Outbox::V2::Throttler::SKIP_STATUS)
34
+ end
35
+
36
+ Success(Sbmt::Outbox::V2::Throttler::NOOP_STATUS)
37
+ rescue => ex
38
+ # noop, just skip any redis / serialization errors
39
+ Failure(ex.message)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/poll_throttler/base"
4
+ require "sbmt/outbox/v2/poll_throttler/composite"
5
+ require "sbmt/outbox/v2/poll_throttler/rate_limited"
6
+ require "sbmt/outbox/v2/poll_throttler/fixed_delay"
7
+ require "sbmt/outbox/v2/poll_throttler/noop"
8
+ require "sbmt/outbox/v2/poll_throttler/redis_queue_size"
9
+ require "sbmt/outbox/v2/poll_throttler/redis_queue_time_lag"
10
+
11
+ module Sbmt
12
+ module Outbox
13
+ module V2
14
+ module PollThrottler
15
+ POLL_TACTICS = %w[noop default low-priority aggressive]
16
+
17
+ def self.build(tactic, redis, poller_config)
18
+ raise "WARN: invalid poller poll tactic provided: #{tactic}, available options: #{POLL_TACTICS}" unless POLL_TACTICS.include?(tactic)
19
+
20
+ if tactic == "default"
21
+ # composite of RateLimited & RedisQueueSize (upper bound only)
22
+ # optimal polling performance for most cases
23
+ Composite.new(throttlers: [
24
+ RedisQueueSize.new(redis: redis, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay),
25
+ RateLimited.new(limit: poller_config.rate_limit, interval: poller_config.rate_interval)
26
+ ])
27
+ elsif tactic == "low-priority"
28
+ # composite of RateLimited & RedisQueueSize (with lower & upper bounds) & RedisQueueTimeLag,
29
+ # delays polling depending on min job queue size threshold
30
+ # and also by min redis queue oldest item lag
31
+ # optimal polling performance for low-intensity data flow
32
+ Composite.new(throttlers: [
33
+ RedisQueueSize.new(redis: redis, min_size: poller_config.min_queue_size, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay),
34
+ RedisQueueTimeLag.new(redis: redis, min_lag: poller_config.min_queue_timelag, delay: poller_config.queue_delay),
35
+ RateLimited.new(limit: poller_config.rate_limit, interval: poller_config.rate_interval)
36
+ ])
37
+ elsif tactic == "aggressive"
38
+ # throttles only by max job queue size, max polling performance
39
+ # optimal polling performance for high-intensity data flow
40
+ RedisQueueSize.new(redis: redis, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay)
41
+ elsif tactic == "noop"
42
+ # no-op, for testing purposes
43
+ Noop.new
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redlock"
4
+ require "sbmt/outbox/v2/box_processor"
5
+ require "sbmt/outbox/v2/redis_job"
6
+ require "sbmt/outbox/v2/poll_throttler"
7
+ require "sbmt/outbox/v2/tasks/poll"
8
+
9
+ module Sbmt
10
+ module Outbox
11
+ module V2
12
+ class Poller < BoxProcessor
13
+ delegate :poller_config, :logger, to: "Sbmt::Outbox"
14
+ delegate :box_worker, to: "Yabeda"
15
+ attr_reader :partitions_count, :lock_timeout, :regular_items_batch_size, :retryable_items_batch_size, :max_buffer_size, :max_batch_size, :throttler
16
+
17
+ def initialize(
18
+ boxes,
19
+ partitions_count: nil,
20
+ threads_count: nil,
21
+ lock_timeout: nil,
22
+ regular_items_batch_size: nil,
23
+ retryable_items_batch_size: nil,
24
+ throttler_tactic: nil,
25
+ redis: nil
26
+ )
27
+ @partitions_count = partitions_count || poller_config.concurrency
28
+ @lock_timeout = lock_timeout || poller_config.general_timeout
29
+
30
+ @regular_items_batch_size = regular_items_batch_size || poller_config.regular_items_batch_size
31
+ @retryable_items_batch_size = retryable_items_batch_size || poller_config.retryable_items_batch_size
32
+ @max_buffer_size = @regular_items_batch_size + @retryable_items_batch_size
33
+ @max_batch_size = @regular_items_batch_size
34
+
35
+ super(boxes: boxes, threads_count: threads_count || poller_config.threads_count, name: "poller", redis: redis)
36
+
37
+ @throttler = PollThrottler.build(throttler_tactic || poller_config.tactic || "default", self.redis, poller_config)
38
+ end
39
+
40
+ def throttle(worker_number, poll_task, result)
41
+ throttler.call(worker_number, poll_task, result)
42
+ end
43
+
44
+ def process_task(_worker_number, task)
45
+ poll(task)
46
+ end
47
+
48
+ private
49
+
50
+ def build_task_queue(boxes)
51
+ scheduled_tasks = boxes.map do |item_class|
52
+ schedule_concurrency = (0...partitions_count).to_a
53
+ schedule_concurrency.map do |partition|
54
+ buckets = item_class.calc_bucket_partitions(partitions_count).fetch(partition)
55
+
56
+ Tasks::Poll.new(
57
+ item_class: item_class,
58
+ worker_name: worker_name,
59
+ partition: partition,
60
+ buckets: buckets
61
+ )
62
+ end
63
+ end.flatten
64
+
65
+ scheduled_tasks.shuffle!
66
+ Queue.new.tap { |queue| scheduled_tasks.each { |task| queue << task } }
67
+ end
68
+
69
+ def lock_task(poll_task)
70
+ lock_manager.lock("#{poll_task.resource_path}:lock", lock_timeout * 1000) do |locked|
71
+ lock_status = locked ? "locked" : "skipped"
72
+ logger.log_debug("poller: lock for #{poll_task}: #{lock_status}")
73
+
74
+ yield(locked ? poll_task : nil)
75
+ end
76
+ end
77
+
78
+ def poll(task)
79
+ lock_timer = Cutoff.new(lock_timeout)
80
+ last_id = 0
81
+
82
+ box_worker.item_execution_runtime.measure(task.yabeda_labels) do
83
+ Outbox.database_switcher.use_slave do
84
+ result = fetch_items(task) do |item|
85
+ box_worker.job_items_counter.increment(task.yabeda_labels)
86
+
87
+ last_id = item.id
88
+ lock_timer.checkpoint!
89
+ end
90
+
91
+ logger.log_debug("poll task #{task}: fetched buckets:#{result.keys.count}, items:#{result.values.sum(0) { |ids| ids.count }}")
92
+
93
+ push_to_redis(task, result) if result.present?
94
+ end
95
+ end
96
+ rescue Cutoff::CutoffExceededError
97
+ box_worker.job_timeout_counter.increment(labels)
98
+ logger.log_info("Lock timeout while processing #{task.resource_key} at id #{last_id}")
99
+ end
100
+
101
+ def fetch_items(task)
102
+ regular_count = 0
103
+ retryable_count = 0
104
+
105
+ # single buffer to preserve item's positions
106
+ poll_buffer = {}
107
+
108
+ fetch_items_with_retries(task, max_batch_size).each do |item|
109
+ if item.errors_count > 0
110
+ # skip if retryable buffer capacity limit reached
111
+ next if retryable_count >= retryable_items_batch_size
112
+
113
+ poll_buffer[item.bucket] ||= []
114
+ poll_buffer[item.bucket] << item.id
115
+
116
+ retryable_count += 1
117
+ else
118
+ poll_buffer[item.bucket] ||= []
119
+ poll_buffer[item.bucket] << item.id
120
+
121
+ regular_count += 1
122
+ end
123
+
124
+ yield(item)
125
+ end
126
+
127
+ box_worker.batches_per_poll_counter.increment(task.yabeda_labels)
128
+
129
+ return {} if poll_buffer.blank?
130
+
131
+ # regular items have priority over retryable ones
132
+ return poll_buffer if regular_count >= regular_items_batch_size
133
+
134
+ # additionally poll regular items only when retryable buffer capacity limit reached
135
+ # and no regular items were found
136
+ if retryable_count >= retryable_items_batch_size && regular_count == 0
137
+ fetch_regular_items(task, regular_items_batch_size).each do |item|
138
+ poll_buffer[item.bucket] ||= []
139
+ poll_buffer[item.bucket] << item.id
140
+
141
+ yield(item)
142
+ end
143
+ box_worker.batches_per_poll_counter.increment(task.yabeda_labels)
144
+ end
145
+
146
+ poll_buffer
147
+ end
148
+
149
+ def fetch_items_with_retries(task, limit)
150
+ task.item_class
151
+ .for_processing
152
+ .where(bucket: task.buckets)
153
+ .order(id: :asc)
154
+ .limit(limit)
155
+ .select(:id, :bucket, :errors_count)
156
+ end
157
+
158
+ def fetch_regular_items(task, limit)
159
+ task.item_class
160
+ .for_processing
161
+ .where(bucket: task.buckets, errors_count: 0)
162
+ .order(id: :asc)
163
+ .limit(limit)
164
+ .select(:id, :bucket)
165
+ end
166
+
167
+ def push_to_redis(poll_task, ids_per_bucket)
168
+ redis.pipelined do |conn|
169
+ ids_per_bucket.each do |bucket, ids|
170
+ redis_job = RedisJob.new(bucket, ids)
171
+
172
+ logger.log_debug("pushing job to redis, items count: #{ids.count}: #{redis_job}")
173
+ conn.call("LPUSH", poll_task.redis_queue, redis_job.serialize)
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redlock"
4
+ require "sbmt/outbox/v2/box_processor"
5
+ require "sbmt/outbox/v2/redis_job"
6
+ require "sbmt/outbox/v2/tasks/process"
7
+
8
+ module Sbmt
9
+ module Outbox
10
+ module V2
11
+ class Processor < BoxProcessor
12
+ delegate :processor_config, :batch_process_middlewares, :logger, to: "Sbmt::Outbox"
13
+ attr_reader :lock_timeout, :brpop_delay
14
+
15
+ def initialize(
16
+ boxes,
17
+ threads_count: nil,
18
+ lock_timeout: nil,
19
+ brpop_delay: nil,
20
+ redis: nil
21
+ )
22
+ @lock_timeout = lock_timeout || processor_config.general_timeout
23
+ @brpop_delay = brpop_delay || processor_config.brpop_delay
24
+
25
+ super(boxes: boxes, threads_count: threads_count || processor_config.threads_count, name: "processor", redis: redis)
26
+ end
27
+
28
+ def process_task(_worker_number, task)
29
+ middlewares = Middleware::Builder.new(batch_process_middlewares)
30
+ middlewares.call(task) { process(task) }
31
+ end
32
+
33
+ private
34
+
35
+ def build_task_queue(boxes)
36
+ # queue size is: boxes_count * threads_count
37
+ # to simplify scheduling per box
38
+ tasks = boxes.map do |item_class|
39
+ (0...threads_count)
40
+ .to_a
41
+ .map { Tasks::Base.new(item_class: item_class, worker_name: worker_name) }
42
+ end.flatten
43
+
44
+ tasks.shuffle!
45
+ Queue.new.tap { |queue| tasks.each { |task| queue << task } }
46
+ end
47
+
48
+ def lock_task(scheduled_task)
49
+ redis_job = fetch_redis_job(scheduled_task)
50
+ return yield(nil) if redis_job.blank?
51
+
52
+ processor_task = Tasks::Process.new(
53
+ item_class: scheduled_task.item_class,
54
+ worker_name: worker_name,
55
+ bucket: redis_job.bucket,
56
+ ids: redis_job.ids
57
+ )
58
+ lock_manager.lock("#{processor_task.resource_path}:lock", lock_timeout * 1000) do |locked|
59
+ lock_status = locked ? "locked" : "skipped"
60
+ logger.log_debug("processor: lock for #{processor_task}: #{lock_status}")
61
+
62
+ yield(locked ? processor_task : nil)
63
+ end
64
+ end
65
+
66
+ def process(task)
67
+ lock_timer = Cutoff.new(lock_timeout)
68
+ last_id = 0
69
+
70
+ box_worker.item_execution_runtime.measure(task.yabeda_labels) do
71
+ Outbox.database_switcher.use_master do
72
+ task.ids.each do |id|
73
+ ProcessItem.call(task.item_class, id, worker_version: task.yabeda_labels[:worker_version])
74
+
75
+ box_worker.job_items_counter.increment(task.yabeda_labels)
76
+ last_id = id
77
+ lock_timer.checkpoint!
78
+ end
79
+ end
80
+ end
81
+ rescue Cutoff::CutoffExceededError
82
+ box_worker.job_timeout_counter.increment(task.yabeda_labels)
83
+ logger.log_info("Lock timeout while processing #{task.resource_key} at id #{last_id}")
84
+ end
85
+
86
+ def fetch_redis_job(scheduled_task)
87
+ _queue, result = redis.blocking_call(redis_block_timeout, "BRPOP", "#{scheduled_task.item_class.box_name}:job_queue", brpop_delay)
88
+ return if result.blank?
89
+
90
+ RedisJob.deserialize!(result)
91
+ rescue => ex
92
+ logger.log_error("error while fetching redis job: #{ex.message}")
93
+ end
94
+
95
+ def redis_block_timeout
96
+ redis.read_timeout + brpop_delay
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module V2
6
+ class RedisJob
7
+ attr_reader :bucket, :timestamp, :ids
8
+
9
+ GENERIC_SEPARATOR = ":"
10
+ IDS_SEPARATOR = ","
11
+
12
+ def initialize(bucket, ids, timestamp = Time.current.to_i)
13
+ @bucket = bucket
14
+ @ids = ids
15
+ @timestamp = timestamp
16
+ end
17
+
18
+ def to_s
19
+ serialize
20
+ end
21
+
22
+ def serialize
23
+ [bucket, timestamp, ids.join(IDS_SEPARATOR)].join(GENERIC_SEPARATOR)
24
+ end
25
+
26
+ def self.deserialize!(value)
27
+ raise "invalid data type: string is required" unless value.is_a?(String)
28
+
29
+ bucket, ts_utc, ids_str, _ = value.split(GENERIC_SEPARATOR)
30
+ raise "invalid data format: bucket or ids are not valid" if bucket.blank? || ts_utc.blank? || ids_str.blank?
31
+
32
+ ts = ts_utc.to_i
33
+
34
+ ids = ids_str.split(IDS_SEPARATOR).map(&:to_i)
35
+ raise "invalid data format: IDs are empty" if ids.blank?
36
+
37
+ new(bucket, ids, ts)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module V2
6
+ module Tasks
7
+ class Base
8
+ attr_reader :item_class, :worker_name, :worker_version, :log_tags, :yabeda_labels
9
+
10
+ def initialize(item_class:, worker_name:, worker_version: 2)
11
+ @item_class = item_class
12
+ @worker_name = worker_name
13
+ @worker_version = worker_version
14
+
15
+ @log_tags = {
16
+ box_type: item_class.box_type,
17
+ box_name: item_class.box_name,
18
+ worker_name: worker_name,
19
+ worker_version: worker_version
20
+ }
21
+
22
+ @yabeda_labels = {
23
+ type: item_class.box_type,
24
+ name: metric_safe(item_class.box_name),
25
+ worker_version: 2,
26
+ worker_name: worker_name
27
+ }
28
+ end
29
+
30
+ def to_h
31
+ result = {}
32
+ instance_variables.each do |iv|
33
+ iv = iv.to_s[1..]
34
+ result[iv.to_sym] = instance_variable_get(:"@#{iv}")
35
+ end
36
+ result
37
+ end
38
+
39
+ private
40
+
41
+ def metric_safe(str)
42
+ str.tr("/", "-")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/tasks/base"
4
+
5
+ module Sbmt
6
+ module Outbox
7
+ module V2
8
+ module Tasks
9
+ class Default < Base
10
+ def to_s
11
+ "#{item_class.box_type}/#{item_class.box_name}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/tasks/base"
4
+
5
+ module Sbmt
6
+ module Outbox
7
+ module V2
8
+ module Tasks
9
+ class Poll < Base
10
+ attr_reader :partition, :buckets, :resource_key, :resource_path, :redis_queue
11
+
12
+ def initialize(item_class:, worker_name:, partition:, buckets:)
13
+ super(item_class: item_class, worker_name: worker_name)
14
+
15
+ @partition = partition
16
+ @buckets = buckets
17
+
18
+ @resource_key = "#{item_class.box_name}:#{partition}"
19
+ @resource_path = "sbmt:outbox:#{worker_name}:#{resource_key}"
20
+ @redis_queue = "#{item_class.box_name}:job_queue"
21
+
22
+ @log_tags = log_tags.merge(box_partition: partition)
23
+
24
+ @yabeda_labels = yabeda_labels.merge(partition: partition)
25
+ end
26
+
27
+ def to_s
28
+ resource_path
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sbmt/outbox/v2/tasks/base"
4
+
5
+ module Sbmt
6
+ module Outbox
7
+ module V2
8
+ module Tasks
9
+ class Process < Base
10
+ attr_reader :partition, :bucket, :ids, :resource_key, :resource_path
11
+
12
+ def initialize(item_class:, worker_name:, bucket:, ids:)
13
+ super(item_class: item_class, worker_name: worker_name)
14
+
15
+ @bucket = bucket
16
+ @ids = ids
17
+
18
+ @resource_key = "#{item_class.box_name}:#{bucket}"
19
+ @resource_path = "sbmt:outbox:#{worker_name}:#{resource_key}"
20
+
21
+ @log_tags = log_tags.merge(bucket: bucket)
22
+ end
23
+
24
+ def to_s
25
+ resource_path
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end