undertow 0.1.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 +7 -0
- data/lib/undertow/buffer.rb +99 -0
- data/lib/undertow/configuration.rb +34 -0
- data/lib/undertow/drain_job.rb +59 -0
- data/lib/undertow/dsl.rb +51 -0
- data/lib/undertow/railtie.rb +21 -0
- data/lib/undertow/registry.rb +45 -0
- data/lib/undertow/trackable.rb +115 -0
- data/lib/undertow/version.rb +5 -0
- data/lib/undertow.rb +52 -0
- data/undertow.gemspec +28 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 53f9ee409c2585ff551f21884f10e8eec276ee750e6f3c12109280cc79fdd934
|
|
4
|
+
data.tar.gz: e343c5be340f08b271e5851a91c0a3c5f363ce28357b5deb83657ae5c242f494
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 28d46d6ddfb4e02ef12dcc001295e9f39034708af79ba12761553d61c32fe96d6d64769cdf45c9df8d3276ff19c9de0080b843ff54cc575e94f7a947db545c99
|
|
7
|
+
data.tar.gz: 49c01b579d3797b755d52c077890753abba9defe3b49ca7e7871564c4ee31627328933a00e0fa0f3e031af16f5e2cb54354516dc277bc87dfc18e68cd464379c
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
# Low-level Redis SET operations used by Trackable callbacks and DrainJob.
|
|
5
|
+
# All methods are no-ops when tracking is disabled or Redis is unavailable.
|
|
6
|
+
module Buffer
|
|
7
|
+
class << self
|
|
8
|
+
def push_pending(model_name, ids)
|
|
9
|
+
return if Undertow.tracking_disabled?
|
|
10
|
+
|
|
11
|
+
with_redis do |r|
|
|
12
|
+
r.sadd(Registry.pending_key(model_name), ids)
|
|
13
|
+
r.sadd(Registry::MODELS_KEY, model_name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def push_deleted(model_name, ids)
|
|
18
|
+
return if Undertow.tracking_disabled?
|
|
19
|
+
|
|
20
|
+
with_redis do |r|
|
|
21
|
+
r.sadd(Registry.deleted_key(model_name), ids)
|
|
22
|
+
r.sadd(Registry::MODELS_KEY, model_name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pop_pending(model_name, count)
|
|
27
|
+
with_redis { |r| r.spop(Registry.pending_key(model_name), count) } || []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def pop_deleted(model_name, count)
|
|
31
|
+
with_redis { |r| r.spop(Registry.deleted_key(model_name), count) } || []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pending_model_names
|
|
35
|
+
with_redis { |r| r.smembers(Registry::MODELS_KEY) } || []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deregister_model(model_name)
|
|
39
|
+
with_redis { |r| r.srem(Registry::MODELS_KEY, model_name) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reregister_model(model_name)
|
|
43
|
+
with_redis { |r| r.sadd(Registry::MODELS_KEY, model_name) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remaining(model_name)
|
|
47
|
+
with_redis do |r|
|
|
48
|
+
r.scard(Registry.pending_key(model_name)) +
|
|
49
|
+
r.scard(Registry.deleted_key(model_name))
|
|
50
|
+
end || 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def restore_pending(model_name, ids)
|
|
54
|
+
with_redis { |r| r.sadd(Registry.pending_key(model_name), ids) } if ids.any?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore_deleted(model_name, ids)
|
|
58
|
+
with_redis { |r| r.sadd(Registry.deleted_key(model_name), ids) } if ids.any?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pending?
|
|
62
|
+
with_redis { |r| r.scard(Registry::MODELS_KEY) > 0 } || false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Acquire the distributed drain lock using SET NX. Returns true if the lock
|
|
66
|
+
# was acquired, false if it was already held. TTL is a safety valve in case
|
|
67
|
+
# the job process dies before releasing it.
|
|
68
|
+
def acquire_drain_lock(ttl: 30)
|
|
69
|
+
lock_key = Undertow.configuration.drain_lock_key
|
|
70
|
+
return true unless lock_key
|
|
71
|
+
|
|
72
|
+
with_redis { |r| r.set(lock_key, '1', nx: true, ex: ttl) } || false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Release the drain lock. Called at the start of DrainJob#perform so the
|
|
76
|
+
# scheduler can enqueue another job for IDs that arrive while this one runs.
|
|
77
|
+
def release_drain_lock
|
|
78
|
+
lock_key = Undertow.configuration.drain_lock_key
|
|
79
|
+
return unless lock_key
|
|
80
|
+
|
|
81
|
+
with_redis { |r| r.del(lock_key) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def with_redis
|
|
87
|
+
client = Undertow.configuration.redis!
|
|
88
|
+
if client.respond_to?(:with)
|
|
89
|
+
client.with { |conn| yield conn }
|
|
90
|
+
else
|
|
91
|
+
yield client
|
|
92
|
+
end
|
|
93
|
+
rescue Redis::BaseConnectionError, Redis::CommandError => e
|
|
94
|
+
Rails.logger.error("Undertow: Redis error: #{e.message}") if defined?(Rails)
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
class Configuration
|
|
5
|
+
# A Redis client or connection pool. Must respond to :sadd, :srem, :smembers,
|
|
6
|
+
# :spop, :scard, :del. Injected by the host application:
|
|
7
|
+
#
|
|
8
|
+
# Undertow.configure { |c| c.redis = Redis.new(url: ENV['REDIS_URL']) }
|
|
9
|
+
#
|
|
10
|
+
attr_accessor :redis
|
|
11
|
+
|
|
12
|
+
# Maximum number of IDs to pop from the buffer per drain per model.
|
|
13
|
+
attr_accessor :max_batch
|
|
14
|
+
|
|
15
|
+
# ActiveJob queue to use for DrainJob.
|
|
16
|
+
attr_accessor :queue_name
|
|
17
|
+
|
|
18
|
+
# Redis key used for the distributed drain lock. The scheduler acquires this
|
|
19
|
+
# lock (SET NX) before enqueueing DrainJob; the job releases it immediately
|
|
20
|
+
# on start so new work arriving mid-drain gets its own job on the next tick.
|
|
21
|
+
# Set to nil to disable lock management entirely.
|
|
22
|
+
attr_accessor :drain_lock_key
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@max_batch = 1_000
|
|
26
|
+
@queue_name = :undertow
|
|
27
|
+
@drain_lock_key = 'undertow:drain:lock'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def redis!
|
|
31
|
+
redis or raise 'Undertow.configuration.redis is not set'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
# Drains the per-model Redis buffers and delivers batches of dirty IDs to
|
|
5
|
+
# each model's configured on_drain handler.
|
|
6
|
+
#
|
|
7
|
+
# Publishes two ActiveSupport::Notifications events:
|
|
8
|
+
# drain.undertow — after a successful on_drain call ({ model:, ids:, deleted_ids: })
|
|
9
|
+
# error.undertow — when on_drain raises ({ model:, exception: })
|
|
10
|
+
class DrainJob < ActiveJob::Base
|
|
11
|
+
queue_as { Undertow.configuration.queue_name }
|
|
12
|
+
|
|
13
|
+
def perform
|
|
14
|
+
# Release the lock before draining so the scheduler can enqueue another job
|
|
15
|
+
# for IDs that arrive while this one is running.
|
|
16
|
+
Buffer.release_drain_lock
|
|
17
|
+
|
|
18
|
+
model_names = Buffer.pending_model_names
|
|
19
|
+
return if model_names.empty?
|
|
20
|
+
|
|
21
|
+
model_names.each { |name| drain_model(name) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def drain_model(model_name)
|
|
27
|
+
max = Undertow.configuration.max_batch
|
|
28
|
+
|
|
29
|
+
# Deregister before popping — any concurrent push will re-add the model,
|
|
30
|
+
# preventing the race where srem fires after a concurrent sadd.
|
|
31
|
+
Buffer.deregister_model(model_name)
|
|
32
|
+
|
|
33
|
+
ids = Buffer.pop_pending(model_name, max)
|
|
34
|
+
deleted_ids = Buffer.pop_deleted(model_name, max)
|
|
35
|
+
return if ids.empty? && deleted_ids.empty?
|
|
36
|
+
|
|
37
|
+
# If the batch was capped, re-register so the next scheduler tick picks up.
|
|
38
|
+
Buffer.reregister_model(model_name) if Buffer.remaining(model_name).positive?
|
|
39
|
+
|
|
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
|
|
43
|
+
|
|
44
|
+
config.on_drain.call(model_name, ids, deleted_ids)
|
|
45
|
+
|
|
46
|
+
ActiveSupport::Notifications.instrument('drain.undertow', {
|
|
47
|
+
model: model_name,
|
|
48
|
+
ids: ids,
|
|
49
|
+
deleted_ids: deleted_ids
|
|
50
|
+
})
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Buffer.restore_pending(model_name, ids) if ids&.any?
|
|
53
|
+
Buffer.restore_deleted(model_name, deleted_ids) if deleted_ids&.any?
|
|
54
|
+
Buffer.reregister_model(model_name)
|
|
55
|
+
ActiveSupport::Notifications.instrument('error.undertow', { model: model_name, exception: e })
|
|
56
|
+
Rails.logger.error("[Undertow::DrainJob] #{model_name}: #{e.message}") if defined?(Rails)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/undertow/dsl.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
# Class-level DSL extended onto ActiveRecord::Base by the Railtie. Any model
|
|
5
|
+
# that calls these methods automatically registers itself with Undertow and
|
|
6
|
+
# gets Trackable behavior wired in at boot — no include needed.
|
|
7
|
+
#
|
|
8
|
+
# class Activity < ApplicationRecord
|
|
9
|
+
# undertow_on_drain ->(model_name, ids, deleted_ids) { ActivityReindexJob.perform_later(ids, deleted_ids) }
|
|
10
|
+
# undertow_skip %w[lock_version searchkick_reindexing]
|
|
11
|
+
#
|
|
12
|
+
# undertow_depends_on :provider, foreign_key: :provider_id, watched_columns: %w[approved mobile]
|
|
13
|
+
# undertow_depends_on :location_series,
|
|
14
|
+
# resolver: ->(ls) { Activity.where(series_id: ls.series_id) },
|
|
15
|
+
# watched_columns: %w[location_id hidden]
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
module DSL
|
|
19
|
+
def undertow_on_drain(callable)
|
|
20
|
+
_undertow_config.on_drain = callable
|
|
21
|
+
_undertow_ensure_trackable!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def undertow_skip(columns)
|
|
25
|
+
_undertow_config.skip_columns = columns
|
|
26
|
+
_undertow_ensure_trackable!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def undertow_depends_on(association, foreign_key: nil, resolver: nil, watched_columns: nil)
|
|
30
|
+
raise ArgumentError, 'provide exactly one of foreign_key: or resolver:' unless foreign_key.nil? ^ resolver.nil?
|
|
31
|
+
|
|
32
|
+
_undertow_config.dependencies << {
|
|
33
|
+
association: association,
|
|
34
|
+
foreign_key: foreign_key,
|
|
35
|
+
resolver: resolver,
|
|
36
|
+
watched_columns: watched_columns
|
|
37
|
+
}.freeze
|
|
38
|
+
_undertow_ensure_trackable!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def _undertow_config
|
|
44
|
+
Registry.register(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def _undertow_ensure_trackable!
|
|
48
|
+
include Undertow::Trackable unless ancestors.include?(Undertow::Trackable)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
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.
|
|
7
|
+
initializer 'undertow.extend_active_record' do
|
|
8
|
+
ActiveSupport.on_load(:active_record) { extend Undertow::DSL }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Wire after_commit callbacks onto every registered model and its dependencies
|
|
12
|
+
# after all models are loaded. Runs on each code reload in development.
|
|
13
|
+
config.to_prepare do
|
|
14
|
+
Registry.all.each do |model_name, config|
|
|
15
|
+
model_name.constantize.register_undertow_callbacks!(config)
|
|
16
|
+
rescue NameError => e
|
|
17
|
+
Rails.logger.warn("[Undertow] Could not load #{model_name}: #{e.message}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
4
|
+
# Holds the declared dependency configuration for every tracked model.
|
|
5
|
+
# Populated at class load time via the DSL (undertow_on_drain, undertow_skip,
|
|
6
|
+
# undertow_depends_on); consumed by the Railtie and DrainJob.
|
|
7
|
+
module Registry
|
|
8
|
+
MODELS_KEY = 'undertow:pending:models'
|
|
9
|
+
|
|
10
|
+
class ModelConfig
|
|
11
|
+
attr_reader :model_name, :dependencies
|
|
12
|
+
attr_accessor :on_drain, :skip_columns
|
|
13
|
+
|
|
14
|
+
def initialize(model_name)
|
|
15
|
+
@model_name = model_name
|
|
16
|
+
@dependencies = []
|
|
17
|
+
@skip_columns = []
|
|
18
|
+
@on_drain = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Returns an existing config or creates a new one for model_name.
|
|
24
|
+
# Called by each DSL macro the first time it fires on a model.
|
|
25
|
+
def register(model_name)
|
|
26
|
+
all[model_name] ||= ModelConfig.new(model_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def all
|
|
30
|
+
@all ||= {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def [](model_name)
|
|
34
|
+
all[model_name]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def key?(model_name)
|
|
38
|
+
all.key?(model_name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pending_key(model_name) = "undertow:pending:#{model_name}"
|
|
42
|
+
def deleted_key(model_name) = "undertow:deleted:#{model_name}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Undertow
|
|
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.
|
|
6
|
+
#
|
|
7
|
+
# Provides class-level callback registration and the skip_columns guard.
|
|
8
|
+
# Callbacks are wired at boot by the Railtie after all models are loaded.
|
|
9
|
+
module Trackable
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
# Columns listed here suppress self-tracking when they are the *only*
|
|
14
|
+
# things that changed — prevents feedback loops from columns updated by
|
|
15
|
+
# the drain handler itself.
|
|
16
|
+
class_attribute :_undertow_ignored_columns, default: [], instance_writer: false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class_methods do
|
|
20
|
+
# Called by the Railtie after all models/associations are loaded.
|
|
21
|
+
# Idempotent — safe to call multiple times (e.g. in reloading environments).
|
|
22
|
+
def register_undertow_callbacks!(config)
|
|
23
|
+
return if @_undertow_callbacks_registered
|
|
24
|
+
|
|
25
|
+
@_undertow_callbacks_registered = true
|
|
26
|
+
|
|
27
|
+
self._undertow_ignored_columns = config.skip_columns
|
|
28
|
+
|
|
29
|
+
_register_self_callbacks!
|
|
30
|
+
(config.dependencies || []).each { |dep| _register_dep_callbacks!(dep) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def _register_self_callbacks!
|
|
36
|
+
after_commit :_push_self_pending, on: %i[create update]
|
|
37
|
+
after_destroy :_push_self_deleted
|
|
38
|
+
after_restore :_push_self_pending if respond_to?(:after_restore)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def _register_dep_callbacks!(dep)
|
|
42
|
+
dep_class = _resolve_dep_class(dep)
|
|
43
|
+
return unless dep_class
|
|
44
|
+
|
|
45
|
+
root_class = self
|
|
46
|
+
watched = dep[:watched_columns].presence # [] treated same as nil — watch all
|
|
47
|
+
|
|
48
|
+
resolver = dep[:resolver] || begin
|
|
49
|
+
fk = dep[:foreign_key]
|
|
50
|
+
->(record) { root_class.where(fk => record.id) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
push_pending = ->(record) {
|
|
54
|
+
ids = resolver.call(record).pluck(:id)
|
|
55
|
+
next unless ids.any?
|
|
56
|
+
|
|
57
|
+
root_class._push_undertow_pending(ids)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Skip create/update callback when watched_columns is set and none changed.
|
|
61
|
+
# Note: saved_changes is empty when touched via belongs_to touch: true (bypasses
|
|
62
|
+
# dirty tracking) — that correctly falls through to skip here.
|
|
63
|
+
dep_class.after_commit on: %i[create update] do
|
|
64
|
+
next if watched && (saved_changes.keys & watched).none?
|
|
65
|
+
|
|
66
|
+
push_pending.call(self)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Dep destroyed — reindex surviving root records. SoftDeletable calls
|
|
70
|
+
# run_callbacks(:destroy), which fires after_destroy, but update_columns does NOT
|
|
71
|
+
# trigger after_commit, so scoping after_commit to [:create, :update] above
|
|
72
|
+
# ensures destroy commits don't double-fire.
|
|
73
|
+
dep_class.after_destroy { push_pending.call(self) }
|
|
74
|
+
|
|
75
|
+
# Dep restored — after_restore is the only hook that fires because restore!
|
|
76
|
+
# uses update_columns, bypassing after_commit.
|
|
77
|
+
if dep_class.respond_to?(:after_restore)
|
|
78
|
+
dep_class.after_restore { push_pending.call(self) }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def _resolve_dep_class(dep)
|
|
83
|
+
if dep[:resolver]
|
|
84
|
+
dep[:association].to_s.classify.constantize
|
|
85
|
+
else
|
|
86
|
+
reflect_on_association(dep[:association])&.klass ||
|
|
87
|
+
dep[:association].to_s.classify.constantize
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
public
|
|
92
|
+
|
|
93
|
+
def _push_undertow_pending(ids)
|
|
94
|
+
Buffer.push_pending(name, ids)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def _push_undertow_deleted(ids)
|
|
98
|
+
Buffer.push_deleted(name, ids)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def _push_self_pending
|
|
105
|
+
ignored = self.class._undertow_ignored_columns
|
|
106
|
+
return if ignored.any? && saved_changes.any? && (saved_changes.keys - ignored).empty?
|
|
107
|
+
|
|
108
|
+
self.class._push_undertow_pending([id])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def _push_self_deleted
|
|
112
|
+
self.class._push_undertow_deleted([id])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/undertow.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_record'
|
|
5
|
+
|
|
6
|
+
require_relative 'undertow/version'
|
|
7
|
+
require_relative 'undertow/configuration'
|
|
8
|
+
require_relative 'undertow/registry'
|
|
9
|
+
require_relative 'undertow/buffer'
|
|
10
|
+
require_relative 'undertow/dsl'
|
|
11
|
+
require_relative 'undertow/trackable'
|
|
12
|
+
require_relative 'undertow/drain_job'
|
|
13
|
+
require_relative 'undertow/railtie' if defined?(Rails::Railtie)
|
|
14
|
+
|
|
15
|
+
module Undertow
|
|
16
|
+
class << self
|
|
17
|
+
def configure
|
|
18
|
+
yield configuration
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Suppress all buffer pushes inside the block. Useful in tests and
|
|
26
|
+
# data migrations where dependency callbacks should not fire.
|
|
27
|
+
def without_tracking
|
|
28
|
+
previous = Thread.current[:undertow_disabled]
|
|
29
|
+
Thread.current[:undertow_disabled] = true
|
|
30
|
+
yield
|
|
31
|
+
ensure
|
|
32
|
+
Thread.current[:undertow_disabled] = previous
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tracking_disabled?
|
|
36
|
+
Thread.current[:undertow_disabled]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Called from the host application's scheduler on each tick. Checks for
|
|
40
|
+
# pending work, acquires the drain lock, and enqueues DrainJob — no other
|
|
41
|
+
# wiring required:
|
|
42
|
+
#
|
|
43
|
+
# every(1.second, 'undertow') { Undertow.tick }
|
|
44
|
+
#
|
|
45
|
+
def tick
|
|
46
|
+
return unless Buffer.pending?
|
|
47
|
+
return unless Buffer.acquire_drain_lock
|
|
48
|
+
|
|
49
|
+
DrainJob.perform_later
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/undertow.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/undertow/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'undertow'
|
|
7
|
+
spec.version = Undertow::VERSION
|
|
8
|
+
spec.authors = ['Nathan Allen']
|
|
9
|
+
spec.email = ['hello@nallenscott.com']
|
|
10
|
+
spec.summary = 'Buffered, dependency-aware change propagation for ActiveRecord models'
|
|
11
|
+
spec.homepage = 'https://github.com/nallenscott/undertow'
|
|
12
|
+
spec.license = 'MIT'
|
|
13
|
+
|
|
14
|
+
spec.metadata = {
|
|
15
|
+
'source_code_uri' => 'https://github.com/nallenscott/undertow',
|
|
16
|
+
'changelog_uri' => 'https://github.com/nallenscott/undertow/blob/main/CHANGELOG.md',
|
|
17
|
+
'bug_tracker_uri' => 'https://github.com/nallenscott/undertow/issues'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
spec.required_ruby_version = '>= 3.0'
|
|
21
|
+
|
|
22
|
+
spec.files = Dir['lib/**/*.rb'] + ['undertow.gemspec']
|
|
23
|
+
|
|
24
|
+
spec.add_dependency 'activerecord', '~> 7.0'
|
|
25
|
+
spec.add_dependency 'activesupport', '~> 7.0'
|
|
26
|
+
spec.add_dependency 'activejob', '~> 7.0'
|
|
27
|
+
spec.add_dependency 'redis', '~> 5.0'
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: undertow
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Allen
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: activejob
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '7.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '7.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: redis
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
description:
|
|
70
|
+
email:
|
|
71
|
+
- hello@nallenscott.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- lib/undertow.rb
|
|
77
|
+
- lib/undertow/buffer.rb
|
|
78
|
+
- lib/undertow/configuration.rb
|
|
79
|
+
- lib/undertow/drain_job.rb
|
|
80
|
+
- lib/undertow/dsl.rb
|
|
81
|
+
- lib/undertow/railtie.rb
|
|
82
|
+
- lib/undertow/registry.rb
|
|
83
|
+
- lib/undertow/trackable.rb
|
|
84
|
+
- lib/undertow/version.rb
|
|
85
|
+
- undertow.gemspec
|
|
86
|
+
homepage: https://github.com/nallenscott/undertow
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata:
|
|
90
|
+
source_code_uri: https://github.com/nallenscott/undertow
|
|
91
|
+
changelog_uri: https://github.com/nallenscott/undertow/blob/main/CHANGELOG.md
|
|
92
|
+
bug_tracker_uri: https://github.com/nallenscott/undertow/issues
|
|
93
|
+
post_install_message:
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '3.0'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.4.19
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Buffered, dependency-aware change propagation for ActiveRecord models
|
|
112
|
+
test_files: []
|