easyop 0.1.1 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +159 -3
- data/README.md +287 -0
- data/lib/easyop/configuration.rb +8 -0
- data/lib/easyop/events/bus/active_support_notifications.rb +82 -0
- data/lib/easyop/events/bus/adapter.rb +119 -0
- data/lib/easyop/events/bus/custom.rb +51 -0
- data/lib/easyop/events/bus/memory.rb +64 -0
- data/lib/easyop/events/bus.rb +66 -0
- data/lib/easyop/events/event.rb +44 -0
- data/lib/easyop/events/registry.rb +128 -0
- data/lib/easyop/plugins/async.rb +12 -0
- data/lib/easyop/plugins/event_handlers.rb +76 -0
- data/lib/easyop/plugins/events.rb +162 -0
- data/lib/easyop/version.rb +1 -1
- data/lib/easyop.rb +11 -0
- metadata +10 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# Inheritable base class for custom bus implementations.
|
|
7
|
+
#
|
|
8
|
+
# Subclass this (not +Bus::Base+) when building a transport adapter for
|
|
9
|
+
# an external message broker, pub/sub system, or any custom delivery
|
|
10
|
+
# mechanism. +Bus::Base+ defines the required interface and glob helpers.
|
|
11
|
+
# +Bus::Adapter+ adds two production-grade utilities on top:
|
|
12
|
+
#
|
|
13
|
+
# _safe_invoke(handler, event) — call a handler, swallow StandardError
|
|
14
|
+
# _compile_pattern(pattern) — glob/string → cached Regexp
|
|
15
|
+
#
|
|
16
|
+
# Both are protected so they are accessible in subclasses but not part
|
|
17
|
+
# of the external bus interface.
|
|
18
|
+
#
|
|
19
|
+
# == Minimum contract
|
|
20
|
+
#
|
|
21
|
+
# Override +#publish+ and +#subscribe+. Override +#unsubscribe+ if your
|
|
22
|
+
# transport supports subscription cancellation.
|
|
23
|
+
#
|
|
24
|
+
# == Example — minimal adapter
|
|
25
|
+
#
|
|
26
|
+
# class PrintBus < Easyop::Events::Bus::Adapter
|
|
27
|
+
# def initialize
|
|
28
|
+
# super
|
|
29
|
+
# @subs = []
|
|
30
|
+
# @mutex = Mutex.new
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# def publish(event)
|
|
34
|
+
# snap = @mutex.synchronize { @subs.dup }
|
|
35
|
+
# snap.each do |sub|
|
|
36
|
+
# _safe_invoke(sub[:handler], event) if _pattern_matches?(sub[:pattern], event.name)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# def subscribe(pattern, &block)
|
|
41
|
+
# handle = { pattern: _compile_pattern(pattern), handler: block }
|
|
42
|
+
# @mutex.synchronize { @subs << handle }
|
|
43
|
+
# handle
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# def unsubscribe(handle)
|
|
47
|
+
# @mutex.synchronize { @subs.delete(handle) }
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# == Example — decorator (wraps another bus)
|
|
52
|
+
#
|
|
53
|
+
# class LoggingBus < Easyop::Events::Bus::Adapter
|
|
54
|
+
# def initialize(inner)
|
|
55
|
+
# super()
|
|
56
|
+
# @inner = inner
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# def publish(event)
|
|
60
|
+
# Rails.logger.info "[bus:publish] #{event.name} payload=#{event.payload}"
|
|
61
|
+
# @inner.publish(event)
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# def subscribe(pattern, &block)
|
|
65
|
+
# @inner.subscribe(pattern, &block)
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# def unsubscribe(handle)
|
|
69
|
+
# @inner.unsubscribe(handle)
|
|
70
|
+
# end
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# Easyop::Events::Registry.bus = LoggingBus.new(Easyop::Events::Bus::Memory.new)
|
|
74
|
+
class Adapter < Base
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
# Call +handler+ with +event+, rescuing any StandardError.
|
|
78
|
+
#
|
|
79
|
+
# Use this inside your +#publish+ implementation so one broken handler
|
|
80
|
+
# never prevents other handlers from running and never surfaces an
|
|
81
|
+
# exception to the event producer.
|
|
82
|
+
#
|
|
83
|
+
# @param handler [#call]
|
|
84
|
+
# @param event [Easyop::Events::Event]
|
|
85
|
+
# @return [void]
|
|
86
|
+
def _safe_invoke(handler, event)
|
|
87
|
+
handler.call(event)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
# Intentionally swallowed — individual handler failures must not
|
|
90
|
+
# propagate to the caller or block remaining handlers.
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Compile +pattern+ into a +Regexp+, memoized per unique pattern value.
|
|
94
|
+
#
|
|
95
|
+
# Results are cached in a per-instance hash so glob→Regexp conversion
|
|
96
|
+
# happens only once regardless of how many events are published.
|
|
97
|
+
#
|
|
98
|
+
# String patterns without wildcards are anchored literally.
|
|
99
|
+
# Glob patterns follow the same rules as +Bus::Base#_glob_to_regex+:
|
|
100
|
+
# "*" — any single dot-separated segment
|
|
101
|
+
# "**" — any sequence of characters including dots
|
|
102
|
+
#
|
|
103
|
+
# @param pattern [String, Regexp]
|
|
104
|
+
# @return [Regexp]
|
|
105
|
+
def _compile_pattern(pattern)
|
|
106
|
+
return pattern if pattern.is_a?(Regexp)
|
|
107
|
+
|
|
108
|
+
@_pattern_cache ||= {}
|
|
109
|
+
@_pattern_cache[pattern] ||=
|
|
110
|
+
if pattern.include?("*")
|
|
111
|
+
_glob_to_regex(pattern)
|
|
112
|
+
else
|
|
113
|
+
Regexp.new("\\A#{Regexp.escape(pattern)}\\z")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# Wraps any user-supplied bus adapter.
|
|
7
|
+
#
|
|
8
|
+
# The adapter must respond to:
|
|
9
|
+
# #publish(event)
|
|
10
|
+
# #subscribe(pattern, &block)
|
|
11
|
+
#
|
|
12
|
+
# Optionally:
|
|
13
|
+
# #unsubscribe(handle)
|
|
14
|
+
#
|
|
15
|
+
# @example Wrapping a RabbitMQ adapter
|
|
16
|
+
# class MyRabbitBus
|
|
17
|
+
# def publish(event) = rabbit.publish(event.to_h, routing_key: event.name)
|
|
18
|
+
# def subscribe(pattern, &block) = rabbit.subscribe(pattern) { |msg| block.call(reconstruct(msg)) }
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Easyop::Events::Registry.bus = MyRabbitBus.new
|
|
22
|
+
#
|
|
23
|
+
# @example Passing via Custom wrapper explicitly
|
|
24
|
+
# Easyop::Events::Registry.bus = Easyop::Events::Bus::Custom.new(MyRabbitBus.new)
|
|
25
|
+
class Custom < Base
|
|
26
|
+
# @param adapter [Object] must respond to #publish and #subscribe
|
|
27
|
+
# @raise [ArgumentError] if adapter does not meet the interface
|
|
28
|
+
def initialize(adapter)
|
|
29
|
+
unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
|
|
30
|
+
raise ArgumentError,
|
|
31
|
+
"Custom bus adapter must respond to #publish(event) and " \
|
|
32
|
+
"#subscribe(pattern, &block). Got: #{adapter.inspect}"
|
|
33
|
+
end
|
|
34
|
+
@adapter = adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def publish(event)
|
|
38
|
+
@adapter.publish(event)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def subscribe(pattern, &block)
|
|
42
|
+
@adapter.subscribe(pattern, &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unsubscribe(handle)
|
|
46
|
+
@adapter.unsubscribe(handle) if @adapter.respond_to?(:unsubscribe)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
module Bus
|
|
6
|
+
# In-process, synchronous bus. Default adapter — requires no external gems.
|
|
7
|
+
#
|
|
8
|
+
# Thread-safe: subscriptions are protected by a Mutex, and publish snapshots
|
|
9
|
+
# the subscriber list before calling handlers so the lock is not held
|
|
10
|
+
# during handler execution (prevents deadlocks).
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# bus = Easyop::Events::Bus::Memory.new
|
|
14
|
+
# bus.subscribe("order.placed") { |e| puts e.name }
|
|
15
|
+
# bus.publish(Easyop::Events::Event.new(name: "order.placed"))
|
|
16
|
+
class Memory < Base
|
|
17
|
+
def initialize
|
|
18
|
+
@subscribers = []
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Deliver +event+ to all matching subscribers.
|
|
23
|
+
# Handlers are called outside the lock; failures in individual handlers
|
|
24
|
+
# are swallowed so other handlers still run.
|
|
25
|
+
#
|
|
26
|
+
# @param event [Easyop::Events::Event]
|
|
27
|
+
def publish(event)
|
|
28
|
+
subs = @mutex.synchronize { @subscribers.dup }
|
|
29
|
+
subs.each do |sub|
|
|
30
|
+
sub[:handler].call(event) if _pattern_matches?(sub[:pattern], event.name)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# Individual handler failures must not prevent other handlers from running.
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Register a handler block for events matching +pattern+.
|
|
37
|
+
#
|
|
38
|
+
# @param pattern [String, Regexp] exact name, glob, or Regexp
|
|
39
|
+
# @return [Hash] subscription handle (pass to #unsubscribe to remove)
|
|
40
|
+
def subscribe(pattern, &block)
|
|
41
|
+
entry = { pattern: pattern, handler: block }
|
|
42
|
+
@mutex.synchronize { @subscribers << entry }
|
|
43
|
+
entry
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Remove a previously registered subscription.
|
|
47
|
+
# @param handle [Hash] the value returned by #subscribe
|
|
48
|
+
def unsubscribe(handle)
|
|
49
|
+
@mutex.synchronize { @subscribers.delete(handle) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Remove all subscriptions. Useful in tests.
|
|
53
|
+
def clear!
|
|
54
|
+
@mutex.synchronize { @subscribers.clear }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer] number of active subscriptions
|
|
58
|
+
def subscriber_count
|
|
59
|
+
@mutex.synchronize { @subscribers.size }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# Namespace for bus adapters.
|
|
6
|
+
# All adapters inherit from Easyop::Events::Bus::Base.
|
|
7
|
+
module Bus
|
|
8
|
+
# Abstract base for bus adapters.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses must implement:
|
|
11
|
+
# #publish(event) — deliver event to matching subscribers
|
|
12
|
+
# #subscribe(pattern, &block) — register a handler block
|
|
13
|
+
#
|
|
14
|
+
# Optionally override:
|
|
15
|
+
# #unsubscribe(handle) — remove a subscription (handle is return value of #subscribe)
|
|
16
|
+
class Base
|
|
17
|
+
# @param event [Easyop::Events::Event]
|
|
18
|
+
def publish(event)
|
|
19
|
+
raise NotImplementedError, "#{self.class}#publish must be implemented"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param pattern [String, Regexp] event name, glob ("order.*"), or Regexp
|
|
23
|
+
# @yield [event] called when a matching event is published
|
|
24
|
+
# @return [Object] subscription handle (adapter-specific)
|
|
25
|
+
def subscribe(pattern, &block)
|
|
26
|
+
raise NotImplementedError, "#{self.class}#subscribe must be implemented"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Remove a subscription created by #subscribe.
|
|
30
|
+
# @param handle [Object] the value returned by #subscribe
|
|
31
|
+
def unsubscribe(handle)
|
|
32
|
+
# default no-op — adapters may override
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Returns true when +pattern+ matches +event_name+.
|
|
38
|
+
# Supports exact strings, glob patterns ("order.*", "order.**"), and Regexp.
|
|
39
|
+
#
|
|
40
|
+
# Glob rules:
|
|
41
|
+
# "*" — matches any segment that doesn't cross a dot
|
|
42
|
+
# "**" — matches any string including dots (greedy)
|
|
43
|
+
#
|
|
44
|
+
# @param pattern [String, Regexp]
|
|
45
|
+
# @param event_name [String]
|
|
46
|
+
def _pattern_matches?(pattern, event_name)
|
|
47
|
+
case pattern
|
|
48
|
+
when Regexp
|
|
49
|
+
pattern.match?(event_name)
|
|
50
|
+
when String
|
|
51
|
+
pattern.include?("*") ? _glob_to_regex(pattern).match?(event_name) : pattern == event_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param glob [String] e.g. "order.*" or "warehouse.**"
|
|
56
|
+
# @return [Regexp]
|
|
57
|
+
def _glob_to_regex(glob)
|
|
58
|
+
escaped = Regexp.escape(glob)
|
|
59
|
+
.gsub("\\*\\*", ".+")
|
|
60
|
+
.gsub("\\*", "[^.]+")
|
|
61
|
+
Regexp.new("\\A#{escaped}\\z")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# An immutable domain event value object.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# event = Easyop::Events::Event.new(
|
|
9
|
+
# name: "order.placed",
|
|
10
|
+
# payload: { order_id: 42, total: 9900 },
|
|
11
|
+
# source: "PlaceOrder"
|
|
12
|
+
# )
|
|
13
|
+
# event.name # => "order.placed"
|
|
14
|
+
# event.payload # => { order_id: 42, total: 9900 }
|
|
15
|
+
# event.frozen? # => true
|
|
16
|
+
class Event
|
|
17
|
+
attr_reader :name, :payload, :metadata, :timestamp, :source
|
|
18
|
+
|
|
19
|
+
# @param name [String] event name, e.g. "order.placed"
|
|
20
|
+
# @param payload [Hash] domain data extracted from ctx
|
|
21
|
+
# @param metadata [Hash] extra envelope data (correlation_id, etc.)
|
|
22
|
+
# @param timestamp [Time] defaults to Time.now
|
|
23
|
+
# @param source [String] class name of the emitting operation
|
|
24
|
+
def initialize(name:, payload: {}, metadata: {}, timestamp: nil, source: nil)
|
|
25
|
+
@name = name.to_s.freeze
|
|
26
|
+
@payload = payload.freeze
|
|
27
|
+
@metadata = metadata.freeze
|
|
28
|
+
@timestamp = (timestamp || Time.now).freeze
|
|
29
|
+
@source = source&.freeze
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash] serializable hash representation
|
|
34
|
+
def to_h
|
|
35
|
+
{ name: @name, payload: @payload, metadata: @metadata,
|
|
36
|
+
timestamp: @timestamp, source: @source }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inspect
|
|
40
|
+
"#<Easyop::Events::Event name=#{@name.inspect} source=#{@source.inspect}>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Events
|
|
5
|
+
# Thread-safe global registry for the event bus and handler subscriptions.
|
|
6
|
+
#
|
|
7
|
+
# The Registry is the coordination point between the Events plugin (producer)
|
|
8
|
+
# and the EventHandlers plugin (subscriber). Neither plugin references the other
|
|
9
|
+
# directly — they communicate only through the bus managed here.
|
|
10
|
+
#
|
|
11
|
+
# Configure the bus once at boot (e.g. in a Rails initializer):
|
|
12
|
+
#
|
|
13
|
+
# Easyop::Events::Registry.bus = :memory # default, in-process
|
|
14
|
+
# Easyop::Events::Registry.bus = :active_support # ActiveSupport::Notifications
|
|
15
|
+
# Easyop::Events::Registry.bus = MyRabbitBus.new # custom adapter
|
|
16
|
+
#
|
|
17
|
+
# Or via the global config:
|
|
18
|
+
#
|
|
19
|
+
# Easyop.configure { |c| c.event_bus = :active_support }
|
|
20
|
+
#
|
|
21
|
+
# IMPORTANT: Configure the bus BEFORE handler classes are loaded (before
|
|
22
|
+
# autoloading runs in Rails). Subscriptions are registered at class load time
|
|
23
|
+
# and are bound to whatever bus is active at that moment.
|
|
24
|
+
class Registry
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Set the global bus adapter.
|
|
29
|
+
#
|
|
30
|
+
# @param bus_or_symbol [:memory, :active_support, Bus::Base, Object]
|
|
31
|
+
def bus=(bus_or_symbol)
|
|
32
|
+
@mutex.synchronize { @bus = _resolve_bus(bus_or_symbol) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the active bus adapter. Defaults to a Memory bus.
|
|
36
|
+
#
|
|
37
|
+
# Falls back to Easyop.config.event_bus if set, then :memory.
|
|
38
|
+
#
|
|
39
|
+
# @return [Bus::Base]
|
|
40
|
+
def bus
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@bus ||= _resolve_bus(
|
|
43
|
+
defined?(Easyop.config) && Easyop.config.respond_to?(:event_bus) && Easyop.config.event_bus ||
|
|
44
|
+
:memory
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a handler operation as a subscriber for +pattern+.
|
|
50
|
+
#
|
|
51
|
+
# Called automatically by the EventHandlers plugin when `on` is declared.
|
|
52
|
+
#
|
|
53
|
+
# @param pattern [String, Regexp] event name or glob
|
|
54
|
+
# @param handler_class [Class] operation class to invoke
|
|
55
|
+
# @param async [Boolean] use call_async (requires Async plugin)
|
|
56
|
+
# @param options [Hash] e.g. queue: "low"
|
|
57
|
+
def register_handler(pattern:, handler_class:, async: false, **options)
|
|
58
|
+
entry = { pattern: pattern, handler_class: handler_class,
|
|
59
|
+
async: async, options: options }
|
|
60
|
+
|
|
61
|
+
@mutex.synchronize { _subscriptions << entry }
|
|
62
|
+
|
|
63
|
+
bus.subscribe(pattern) { |event| _dispatch(event, entry) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns a copy of all registered handler entries (for introspection).
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Hash>]
|
|
69
|
+
def subscriptions
|
|
70
|
+
@mutex.synchronize { _subscriptions.dup }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Reset the registry: drop all subscriptions and replace the bus with a
|
|
74
|
+
# fresh Memory instance. Intended for use in tests (called in before/after hooks).
|
|
75
|
+
def reset!
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@bus = nil
|
|
78
|
+
@subscriptions = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def _subscriptions
|
|
85
|
+
@subscriptions ||= []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @api private — exposed for specs
|
|
89
|
+
def _dispatch(event, entry)
|
|
90
|
+
handler_class = entry[:handler_class]
|
|
91
|
+
|
|
92
|
+
if entry[:async] && handler_class.respond_to?(:call_async)
|
|
93
|
+
# Serialize Event to a plain hash for ActiveJob compatibility.
|
|
94
|
+
attrs = event.payload.merge(event_data: event.to_h)
|
|
95
|
+
queue = entry[:options][:queue]
|
|
96
|
+
handler_class.call_async(attrs, **(queue ? { queue: queue } : {}))
|
|
97
|
+
else
|
|
98
|
+
# Sync: pass Event object directly — ctx.event is the live Event instance.
|
|
99
|
+
handler_class.call(event: event, **event.payload)
|
|
100
|
+
end
|
|
101
|
+
rescue StandardError
|
|
102
|
+
# Handler failures must not propagate back to the publisher.
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def _resolve_bus(bus_or_symbol)
|
|
106
|
+
case bus_or_symbol
|
|
107
|
+
when :memory
|
|
108
|
+
Bus::Memory.new
|
|
109
|
+
when :active_support
|
|
110
|
+
Bus::ActiveSupportNotifications.new
|
|
111
|
+
when Bus::Base
|
|
112
|
+
bus_or_symbol
|
|
113
|
+
when nil
|
|
114
|
+
Bus::Memory.new
|
|
115
|
+
else
|
|
116
|
+
if bus_or_symbol.respond_to?(:publish) && bus_or_symbol.respond_to?(:subscribe)
|
|
117
|
+
Bus::Custom.new(bus_or_symbol)
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"Unknown bus: #{bus_or_symbol.inspect}. " \
|
|
121
|
+
"Use :memory, :active_support, or a bus adapter instance."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/easyop/plugins/async.rb
CHANGED
|
@@ -27,6 +27,18 @@ module Easyop
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
module ClassMethods
|
|
30
|
+
# Declares a non-default queue for this operation class and its subclasses.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# class Weather::BaseOperation < ApplicationOperation
|
|
34
|
+
# queue :weather
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @param name [String, Symbol] queue name
|
|
38
|
+
def queue(name)
|
|
39
|
+
@_async_default_queue = name.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
# Enqueue the operation as a background job.
|
|
31
43
|
#
|
|
32
44
|
# MyOp.call_async(email: "x@y.com")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyop
|
|
4
|
+
module Plugins
|
|
5
|
+
# Subscribes an operation class to handle domain events.
|
|
6
|
+
#
|
|
7
|
+
# The handler operation receives the event payload merged into ctx, plus
|
|
8
|
+
# ctx.event (the Easyop::Events::Event object itself for sync dispatch).
|
|
9
|
+
#
|
|
10
|
+
# Basic usage:
|
|
11
|
+
#
|
|
12
|
+
# class SendOrderConfirmation < ApplicationOperation
|
|
13
|
+
# plugin Easyop::Plugins::EventHandlers
|
|
14
|
+
#
|
|
15
|
+
# on "order.placed"
|
|
16
|
+
#
|
|
17
|
+
# def call
|
|
18
|
+
# event = ctx.event # Easyop::Events::Event
|
|
19
|
+
# order_id = ctx.order_id # payload keys merged into ctx
|
|
20
|
+
# OrderMailer.confirm(order_id).deliver_later
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Async dispatch (requires Easyop::Plugins::Async also installed):
|
|
25
|
+
#
|
|
26
|
+
# class IndexOrder < ApplicationOperation
|
|
27
|
+
# plugin Easyop::Plugins::Async, queue: "indexing"
|
|
28
|
+
# plugin Easyop::Plugins::EventHandlers
|
|
29
|
+
#
|
|
30
|
+
# on "order.*", async: true
|
|
31
|
+
# on "inventory.**", async: true, queue: "low"
|
|
32
|
+
#
|
|
33
|
+
# def call
|
|
34
|
+
# # For async dispatch ctx.event_data is a Hash (serialized for ActiveJob).
|
|
35
|
+
# # Reconstruct if needed: Easyop::Events::Event.new(**ctx.event_data)
|
|
36
|
+
# SearchIndex.reindex(ctx.order_id)
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# Wildcard patterns:
|
|
41
|
+
# "order.*" — matches order.placed, order.shipped (not order.payment.failed)
|
|
42
|
+
# "warehouse.**" — matches warehouse.stock.updated, warehouse.zone.moved, etc.
|
|
43
|
+
module EventHandlers
|
|
44
|
+
def self.install(base, **_options)
|
|
45
|
+
base.extend(ClassMethods)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module ClassMethods
|
|
49
|
+
# Subscribe this operation to events matching +pattern+.
|
|
50
|
+
#
|
|
51
|
+
# Registration happens at class-load time and is bound to the bus that is
|
|
52
|
+
# active when the class is evaluated. Configure the bus before loading
|
|
53
|
+
# handler classes (e.g. in a Rails initializer that runs before autoloading).
|
|
54
|
+
#
|
|
55
|
+
# @param pattern [String, Regexp] event name or glob
|
|
56
|
+
# @param async [Boolean] enqueue via call_async (requires Async plugin)
|
|
57
|
+
# @param options [Hash] e.g. queue: "low" (overrides Async default)
|
|
58
|
+
def on(pattern, async: false, **options)
|
|
59
|
+
_event_handler_registrations << { pattern: pattern, async: async, options: options }
|
|
60
|
+
|
|
61
|
+
Easyop::Events::Registry.register_handler(
|
|
62
|
+
pattern: pattern,
|
|
63
|
+
handler_class: self,
|
|
64
|
+
async: async,
|
|
65
|
+
**options
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private — list of registrations declared on this class
|
|
70
|
+
def _event_handler_registrations
|
|
71
|
+
@_event_handler_registrations ||= []
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|