undertow 0.4.0 → 0.5.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: 1aa09fe4d1c2e088f4b7ca874175db916486f8a6296d49af88544bec8ec254be
4
- data.tar.gz: 30115bb681233cf045251670a9f2ed369e6290340414f5f031f292cd2e26fe23
3
+ metadata.gz: 8eaf3f2d446514a0b894990d2b5f0d5151a9664782af31533dfb38a1954b60b1
4
+ data.tar.gz: 9bdd7da4f3e68d80638e79911ce67fe20a56b689ce048835836eb2e6c90f3d69
5
5
  SHA512:
6
- metadata.gz: a68ec79086258526eb24fbd2690b5df15701975706ee4ae09bbbd800d4e024d1b9c4546d7df03f465de7bbdcd4efc6252b73ac2244252b74d6f93707ea5dff1b
7
- data.tar.gz: 9406d057b05d1a76fade09a0d3b3b698d3503a11bccfdd68591f4eead07bce128dfd3b431fbf4e6078eac092bc4f641a51d0dda036bdd200ab928dbec24155af
6
+ metadata.gz: 3f45df71d291f76c94a4f6550734ef86dbfb8bbccb8d85011e7bb6c144ea78098471e683cf59fc9860858388517ac570e5d65b0eeea215a4180443ab029fd9cb
7
+ data.tar.gz: 6ecdd5f94d0500e8c16ca9f127c3aed178f517be9006d589b3a28695d144931a4b08ac9e9e31567fe0b2d94fb5b2a5d1fe8ca6a58c71a864b04c6d0727c30e29
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Undertow
4
4
  # Drains the per-model Redis buffers and delivers batches of dirty IDs to
5
- # each model's configured on_drain handler.
5
+ # each model's configured sinks.
6
6
  #
7
- # Publishes two ActiveSupport::Notifications events:
8
- # drain.undertow , after a successful on_drain call ({ model:, upserted_ids:, deleted_ids:, duration_ms: })
9
- # error.undertow , when on_drain raises ({ model:, exception: })
7
+ # Publishes two ActiveSupport::Notifications events, once per sink (per chunk,
8
+ # if a sink's max_batch_size is smaller than the popped batch):
9
+ # drain.undertow , after a successful sink call ({ model:, sink:, upserted_ids:, deleted_ids:, duration_ms: })
10
+ # error.undertow , when a sink raises ({ model:, sink:, exception: })
10
11
  class DrainJob < ActiveJob::Base
11
12
  queue_as { Undertow.configuration.queue_name }
12
13
 
@@ -26,6 +27,10 @@ module Undertow
26
27
  def drain_model(model_name)
27
28
  max = Undertow.configuration.max_batch
28
29
 
30
+ config = Registry[model_name]
31
+ raise "No Undertow config registered for #{model_name}" unless config
32
+ raise "#{model_name} has no undertow_sink configured" if config.sinks.empty?
33
+
29
34
  # Deregister before popping, any concurrent push will re-add the model,
30
35
  # preventing the race where srem fires after a concurrent sadd.
31
36
  Buffer.deregister_model(model_name)
@@ -37,26 +42,45 @@ module Undertow
37
42
  # If the batch was capped, re-register so the next scheduler tick picks up.
38
43
  Buffer.reregister_model(model_name) if Buffer.remaining(model_name).positive?
39
44
 
40
- config = Registry[model_name]
41
- raise "No Undertow config registered for #{model_name}" unless config
42
- raise "#{model_name} is missing undertow_on_drain" unless config.on_drain
45
+ current_sink = nil
46
+ config.sinks.each do |sink_name, sink|
47
+ current_sink = sink_name
48
+ chunk_size = sink[:max_batch_size] || max
43
49
 
44
- duration_ms = measure_ms { config.on_drain.call(model_name, upserted_ids, deleted_ids) }
50
+ each_sink_chunk(upserted_ids, deleted_ids, chunk_size) do |chunk_upserted, chunk_deleted|
51
+ duration_ms = measure_ms { sink[:handler].call(model_name, chunk_upserted, chunk_deleted) }
45
52
 
46
- ActiveSupport::Notifications.instrument('drain.undertow', {
47
- model: model_name,
48
- upserted_ids: upserted_ids,
49
- deleted_ids: deleted_ids,
50
- duration_ms: duration_ms
51
- })
53
+ ActiveSupport::Notifications.instrument('drain.undertow', {
54
+ model: model_name,
55
+ sink: sink_name,
56
+ upserted_ids: chunk_upserted,
57
+ deleted_ids: chunk_deleted,
58
+ duration_ms: duration_ms
59
+ })
60
+ end
61
+ end
52
62
  rescue StandardError => e
53
63
  Buffer.restore_pending(model_name, upserted_ids) if upserted_ids&.any?
54
64
  Buffer.restore_deleted(model_name, deleted_ids) if deleted_ids&.any?
55
65
  Buffer.reregister_model(model_name)
56
- ActiveSupport::Notifications.instrument('error.undertow', { model: model_name, exception: e })
66
+ ActiveSupport::Notifications.instrument('error.undertow', { model: model_name, sink: current_sink, exception: e })
57
67
  Rails.logger.error("[Undertow::DrainJob] #{model_name}: #{e.message}") if defined?(Rails)
58
68
  end
59
69
 
70
+ # Splits upserted_ids/deleted_ids into chunks of at most chunk_size combined
71
+ # items, yielding each chunk as (chunk_upserted, chunk_deleted). Tags each ID
72
+ # with which array it came from, concatenates into one list, slices that,
73
+ # then splits each slice back apart, so a single chunk_size limit governs
74
+ # the combined total regardless of the upserted/deleted split.
75
+ def each_sink_chunk(upserted_ids, deleted_ids, chunk_size)
76
+ tagged = upserted_ids.map { |id| [true, id] } + deleted_ids.map { |id| [false, id] }
77
+
78
+ tagged.each_slice(chunk_size) do |slice|
79
+ chunk_upserted, chunk_deleted = slice.partition { |upserted, _| upserted }
80
+ yield chunk_upserted.map(&:last), chunk_deleted.map(&:last)
81
+ end
82
+ end
83
+
60
84
  def measure_ms
61
85
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
86
  yield
data/lib/undertow/dsl.rb CHANGED
@@ -6,8 +6,8 @@ module Undertow
6
6
  # gets Trackable behavior wired in at boot, no include needed.
7
7
  #
8
8
  # class Post < ApplicationRecord
9
- # undertow_on_drain ->(model_name, upserted_ids, deleted_ids) { PostSyncJob.perform_later(upserted_ids, deleted_ids) }
10
- # undertow_skip %w[view_count updated_at]
9
+ # undertow_sink(:search_index) { |model_name, upserted_ids, deleted_ids| PostSyncJob.perform_later(upserted_ids, deleted_ids) }
10
+ # undertow_skip %w[view_count updated_at]
11
11
  #
12
12
  # undertow_depends_on :author, foreign_key: :author_id, watched_columns: %w[name bio]
13
13
  # undertow_depends_on :tag,
@@ -16,8 +16,21 @@ module Undertow
16
16
  # end
17
17
  #
18
18
  module DSL
19
- def undertow_on_drain(callable)
20
- _undertow_config.on_drain = callable
19
+ # Registers a named sink. Call once per sink; every sink on a model receives
20
+ # the same (model_name, upserted_ids, deleted_ids) on each drain, so the
21
+ # block decides what to do with them, e.g. reindex a search index or publish
22
+ # to Kafka.
23
+ #
24
+ # `max_batch_size:` bounds how many IDs this sink's block receives per call.
25
+ # Defaults to `Undertow.configuration.max_batch`, the same size as the
26
+ # popped batch, meaning the block gets called once with everything. Set it
27
+ # lower when this sink's downstream call has a tighter limit than other
28
+ # sinks on the same model; DrainJob will call the block multiple times,
29
+ # chunked to this size, without affecting any other sink's batch size.
30
+ def undertow_sink(name, max_batch_size: nil, &handler)
31
+ raise ArgumentError, 'undertow_sink requires a block' unless handler
32
+
33
+ _undertow_config.sinks[name.to_sym] = { max_batch_size: max_batch_size, handler: handler }
21
34
  _undertow_ensure_trackable!
22
35
  end
23
36
 
@@ -3,7 +3,7 @@
3
3
  module Undertow
4
4
  class Railtie < Rails::Railtie
5
5
  # Extend ActiveRecord::Base with the Undertow DSL so any model can call
6
- # undertow_on_drain, undertow_skip, and undertow_depends_on in its class body.
6
+ # undertow_sink, undertow_skip, and undertow_depends_on in its class body.
7
7
  initializer 'undertow.extend_active_record' do
8
8
  ActiveSupport.on_load(:active_record) { extend Undertow::DSL }
9
9
  end
@@ -2,20 +2,20 @@
2
2
 
3
3
  module Undertow
4
4
  # Holds the declared dependency configuration for every tracked model.
5
- # Populated at class load time via the DSL (undertow_on_drain, undertow_skip,
5
+ # Populated at class load time via the DSL (undertow_sink, undertow_skip,
6
6
  # undertow_depends_on); consumed by the Railtie and DrainJob.
7
7
  module Registry
8
8
  MODELS_KEY = 'undertow:pending:models'
9
9
 
10
10
  class ModelConfig
11
- attr_reader :model_name, :dependencies
12
- attr_accessor :on_drain, :skip_columns
11
+ attr_reader :model_name, :dependencies, :sinks
12
+ attr_accessor :skip_columns
13
13
 
14
14
  def initialize(model_name)
15
15
  @model_name = model_name
16
16
  @dependencies = []
17
17
  @skip_columns = []
18
- @on_drain = nil
18
+ @sinks = {}
19
19
  end
20
20
  end
21
21
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Undertow
4
4
  # ActiveRecord concern mixed in automatically when a model uses the Undertow DSL
5
- # (undertow_on_drain, undertow_skip, undertow_depends_on). Never included manually.
5
+ # (undertow_sink, undertow_skip, undertow_depends_on). Never included manually.
6
6
  #
7
7
  # Provides class-level callback registration and dependency push handlers, plus
8
8
  # instance-level self-tracking handlers. Callbacks are wired at boot by the Railtie
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Undertow
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: undertow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Allen