event_engine-delivery 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/CHANGELOG.md +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +493 -0
- data/Rakefile +9 -0
- data/app/assets/config/event_engine_manifest.js +1 -0
- data/app/assets/stylesheets/event_engine/application.css +15 -0
- data/app/controllers/event_engine/application_controller.rb +4 -0
- data/app/helpers/event_engine/application_helper.rb +4 -0
- data/app/jobs/event_engine/application_job.rb +4 -0
- data/app/jobs/event_engine/outbox_cleanup_job.rb +13 -0
- data/app/jobs/event_engine/publish_outbox_events_job.rb +16 -0
- data/app/mailers/event_engine/application_mailer.rb +6 -0
- data/app/models/event_engine/application_record.rb +5 -0
- data/app/models/event_engine/outbox_event.rb +123 -0
- data/app/views/layouts/event_engine/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20251216190654_create_event_engine_outbox_events.rb +9 -0
- data/db/migrate/20251216194009_add_event_type_to_event_engine_outbox_events.rb +5 -0
- data/db/migrate/20251216201941_add_payload_to_event_engine_outbox_events.rb +9 -0
- data/db/migrate/20251216203339_add_published_at_to_event_engine_outbox_events.rb +5 -0
- data/db/migrate/20251216213819_add_idempotency_key_to_event_engine_outbox_events.rb +6 -0
- data/db/migrate/20251217155422_add_attempts_to_event_engine_outbox_events.rb +5 -0
- data/db/migrate/20251217174013_add_dead_lettered_at_to_event_engine_outbox_events.rb +5 -0
- data/db/migrate/20251220213944_add_event_version_to_outbox_events.rb +20 -0
- data/db/migrate/20251221193107_add_occurred_at_and_metadata_to_outbox_events.rb +6 -0
- data/db/migrate/20260127000000_add_indexes_to_event_engine_outbox_events.rb +8 -0
- data/db/migrate/20260212000001_add_constraints_and_indexes_to_event_engine_outbox_events.rb +6 -0
- data/db/migrate/20260212000002_add_error_context_to_event_engine_outbox_events.rb +6 -0
- data/db/migrate/20260212000003_add_aggregate_columns_to_event_engine_outbox_events.rb +9 -0
- data/db/migrate/20260212000005_add_process_type_to_event_engine_outbox_events.rb +5 -0
- data/db/migrate/20260921000000_add_subject_and_domain_to_event_engine_outbox_events.rb +6 -0
- data/lib/event_engine/cloud/api_client.rb +62 -0
- data/lib/event_engine/cloud/batch.rb +50 -0
- data/lib/event_engine/cloud/reporter.rb +155 -0
- data/lib/event_engine/cloud/serializer.rb +56 -0
- data/lib/event_engine/cloud/subscribers.rb +46 -0
- data/lib/event_engine/definition_transport_check.rb +35 -0
- data/lib/event_engine/delivery/configuration.rb +65 -0
- data/lib/event_engine/delivery/engine.rb +25 -0
- data/lib/event_engine/delivery/handler.rb +52 -0
- data/lib/event_engine/delivery/version.rb +5 -0
- data/lib/event_engine/delivery.rb +61 -0
- data/lib/event_engine/kafka_producer.rb +16 -0
- data/lib/event_engine/locking_strategy.rb +31 -0
- data/lib/event_engine/outbox_publisher.rb +89 -0
- data/lib/event_engine/outbox_router.rb +58 -0
- data/lib/event_engine/outbox_writer.rb +7 -0
- data/lib/event_engine/transports/in_memory_transport.rb +27 -0
- data/lib/event_engine/transports/kafka.rb +45 -0
- data/lib/event_engine/transports/null_transport.rb +27 -0
- data/lib/event_engine-delivery.rb +20 -0
- data/lib/generators/event_engine/install_generator.rb +45 -0
- data/lib/generators/event_engine/templates/event_schema.rb +10 -0
- data/lib/generators/event_engine/templates/initializer.rb +14 -0
- data/lib/tasks/event_engine_dead_letters.rake +65 -0
- data/lib/tasks/event_engine_outbox.rake +25 -0
- data/lib/tasks/event_engine_schema.rake +49 -0
- data/lib/tasks/event_engine_schema_check.rake +20 -0
- data/lib/tasks/event_engine_tasks.rake +4 -0
- metadata +183 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
# Inspects loaded event definitions at boot and logs a non-blocking warning
|
|
3
|
+
# when a :broker event exists but no real transport is configured (a :broker
|
|
4
|
+
# event publishes to a broker, so it needs one). This is a soft, early signal;
|
|
5
|
+
# the hard failure happens at runtime in {OutboxRouter} when such an event is
|
|
6
|
+
# actually drained.
|
|
7
|
+
module DefinitionTransportCheck
|
|
8
|
+
# @param registry [SchemaRegistry] the loaded registry
|
|
9
|
+
# @param transport [#publish, nil] the configured transport
|
|
10
|
+
# @param logger [Logger] where to write the warning
|
|
11
|
+
# @param processing_rules [ProcessingRules] what decides an event's process type
|
|
12
|
+
# @return [void]
|
|
13
|
+
def self.run(registry:, transport:, logger:, processing_rules: EventEngine.processing_rules)
|
|
14
|
+
return if real_transport?(transport)
|
|
15
|
+
|
|
16
|
+
broker = registry.events.select { |name| broker?(registry.schema(name), processing_rules) }
|
|
17
|
+
return if broker.empty?
|
|
18
|
+
|
|
19
|
+
logger.warn(
|
|
20
|
+
"[EventEngine] No transport configured, but these :broker events require one: " \
|
|
21
|
+
"#{broker.join(', ')}. They will raise when published. Set config.transport."
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.broker?(schema, processing_rules)
|
|
26
|
+
processing_rules.for(event_name: schema.event_name, pack: schema.domain) == :broker
|
|
27
|
+
end
|
|
28
|
+
private_class_method :broker?
|
|
29
|
+
|
|
30
|
+
def self.real_transport?(transport)
|
|
31
|
+
!transport.nil? && !(transport.respond_to?(:null?) && transport.null?)
|
|
32
|
+
end
|
|
33
|
+
private_class_method :real_transport?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Delivery
|
|
3
|
+
# Configuration for the delivery layer: transport, outbox publisher behavior,
|
|
4
|
+
# retention, the dashboard, and the cloud reporter.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# EventEngine::Delivery.configure do |config|
|
|
8
|
+
# config.transport = EventEngine::Transports::Kafka.new(producer: producer)
|
|
9
|
+
# config.delivery_adapter = :active_job
|
|
10
|
+
# end
|
|
11
|
+
class Configuration
|
|
12
|
+
class InvalidConfigurationError < StandardError; end
|
|
13
|
+
|
|
14
|
+
attr_accessor :delivery_adapter, :transport, :batch_size, :max_attempts, :retention_period, :dashboard_auth, :logger,
|
|
15
|
+
:cloud_api_key, :cloud_endpoint, :cloud_environment, :cloud_app_name, :cloud_batch_size, :cloud_flush_interval
|
|
16
|
+
|
|
17
|
+
VALID_DELIVERY_ADAPTERS = %i[inline active_job manual].freeze
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@delivery_adapter = :inline
|
|
21
|
+
@transport = ::EventEngine::Transports::NullTransport.new
|
|
22
|
+
@batch_size = 100
|
|
23
|
+
@max_attempts = 5
|
|
24
|
+
@retention_period = nil
|
|
25
|
+
@dashboard_auth = nil
|
|
26
|
+
@logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
|
|
27
|
+
@cloud_api_key = nil
|
|
28
|
+
@cloud_endpoint = "https://api.eventengine.dev/v1/ingest"
|
|
29
|
+
@cloud_environment = nil
|
|
30
|
+
@cloud_app_name = nil
|
|
31
|
+
@cloud_batch_size = 50
|
|
32
|
+
@cloud_flush_interval = 10
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cloud_enabled?
|
|
36
|
+
cloud_api_key.present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate!
|
|
40
|
+
unless VALID_DELIVERY_ADAPTERS.include?(delivery_adapter)
|
|
41
|
+
raise InvalidConfigurationError,
|
|
42
|
+
"Invalid delivery_adapter: #{delivery_adapter.inspect}. Must be one of: #{VALID_DELIVERY_ADAPTERS.join(', ')}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if delivery_adapter == :active_job && (transport.nil? || transport.is_a?(::EventEngine::Transports::NullTransport))
|
|
46
|
+
raise InvalidConfigurationError,
|
|
47
|
+
"Transport must be configured when using :active_job delivery adapter. " \
|
|
48
|
+
"Set config.transport in your EventEngine::Delivery initializer."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if transport && !transport.respond_to?(:publish)
|
|
52
|
+
raise InvalidConfigurationError, "Transport must respond to #publish"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless batch_size.is_a?(Integer) && batch_size > 0
|
|
56
|
+
raise InvalidConfigurationError, "batch_size must be a positive integer, got: #{batch_size.inspect}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
unless max_attempts.is_a?(Integer) && max_attempts > 0
|
|
60
|
+
raise InvalidConfigurationError, "max_attempts must be a positive integer, got: #{max_attempts.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Delivery
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
engine_name "event_engine_delivery"
|
|
5
|
+
|
|
6
|
+
initializer "event_engine.delivery.register_handler" do
|
|
7
|
+
config.after_initialize do
|
|
8
|
+
EventEngine.register_handler(Handler.new, process_types: :all)
|
|
9
|
+
Engine.send(:start_cloud_reporter!)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def start_cloud_reporter!
|
|
17
|
+
return unless Delivery.configuration.cloud_enabled?
|
|
18
|
+
|
|
19
|
+
::EventEngine::Cloud::Subscribers.subscribe!(reporter: ::EventEngine::Cloud::Reporter.instance)
|
|
20
|
+
::EventEngine::Cloud::Reporter.instance.start
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Delivery
|
|
3
|
+
# The delivery handler registered with EventEngine core. Receives a built
|
|
4
|
+
# Event and handles only durable/broker events: it writes them to the outbox
|
|
5
|
+
# and publishes them. :inline and :background events are run by
|
|
6
|
+
# event_engine-subscribers, not here.
|
|
7
|
+
class Handler
|
|
8
|
+
def call(event)
|
|
9
|
+
case process_type_for(event)
|
|
10
|
+
when :inline, :background then nil # run by event_engine-subscribers
|
|
11
|
+
else write_and_publish(event)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def process_type_for(event)
|
|
18
|
+
event.process_type&.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write_and_publish(event)
|
|
22
|
+
outbox_event = OutboxWriter.write(event.to_h)
|
|
23
|
+
|
|
24
|
+
ActiveSupport::Notifications.instrument("event_engine.event_emitted", {
|
|
25
|
+
event_name: outbox_event.event_name,
|
|
26
|
+
event_version: outbox_event.event_version,
|
|
27
|
+
event_id: outbox_event.id,
|
|
28
|
+
idempotency_key: outbox_event.idempotency_key,
|
|
29
|
+
aggregate_type: outbox_event.aggregate_type,
|
|
30
|
+
aggregate_id: outbox_event.aggregate_id,
|
|
31
|
+
aggregate_version: outbox_event.aggregate_version
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
Delivery.enqueue do
|
|
35
|
+
transport = Delivery.configuration.transport
|
|
36
|
+
unless transport
|
|
37
|
+
Rails.logger.warn("[EventEngine::Delivery] No transport configured — event written to outbox but not published. " \
|
|
38
|
+
"Set config.transport in your EventEngine::Delivery initializer to enable publishing.")
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
OutboxPublisher.new(
|
|
43
|
+
router: OutboxRouter.new(transport: transport),
|
|
44
|
+
batch_size: Delivery.configuration.batch_size
|
|
45
|
+
).call
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
outbox_event
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Delivery
|
|
3
|
+
class << self
|
|
4
|
+
def configuration
|
|
5
|
+
@configuration ||= Configuration.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def configure
|
|
9
|
+
yield(configuration)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.enqueue(&block)
|
|
14
|
+
adapter = configuration.delivery_adapter || :inline
|
|
15
|
+
|
|
16
|
+
case adapter
|
|
17
|
+
when :inline
|
|
18
|
+
if block_given?
|
|
19
|
+
if ActiveRecord::Base.connection.transaction_open?
|
|
20
|
+
AfterCommitCallback.register(ActiveRecord::Base.connection, &block)
|
|
21
|
+
else
|
|
22
|
+
yield
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
when :active_job
|
|
26
|
+
PublishOutboxEventsJob.perform_later
|
|
27
|
+
when :manual
|
|
28
|
+
# No automatic delivery. The outbox is drained only by an explicit
|
|
29
|
+
# OutboxPublisher call (e.g. a scheduled job or operator action).
|
|
30
|
+
nil
|
|
31
|
+
else
|
|
32
|
+
raise ArgumentError, "Unknown delivery adapter: #{adapter}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class AfterCommitCallback
|
|
37
|
+
def initialize(&block)
|
|
38
|
+
@callback = block
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def committed!(*)
|
|
42
|
+
@callback.call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def before_committed!(*); end
|
|
46
|
+
def rolledback!(*); end
|
|
47
|
+
|
|
48
|
+
def trigger_transactional_callbacks?
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def has_transactional_callbacks?
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.register(connection, &block)
|
|
57
|
+
connection.current_transaction.add_record(new(&block))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module LockingStrategy
|
|
3
|
+
# Returns a locking strategy appropriate for the current database adapter.
|
|
4
|
+
#
|
|
5
|
+
# @return [NullStrategy, PostgresStrategy]
|
|
6
|
+
def self.for_current_adapter
|
|
7
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
|
8
|
+
case adapter
|
|
9
|
+
when /postgres/
|
|
10
|
+
PostgresStrategy.new
|
|
11
|
+
else
|
|
12
|
+
NullStrategy.new
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# No-op strategy for adapters that don't support SKIP LOCKED (e.g. SQLite).
|
|
17
|
+
class NullStrategy
|
|
18
|
+
def apply(scope)
|
|
19
|
+
scope
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Applies FOR UPDATE SKIP LOCKED for PostgreSQL to prevent duplicate
|
|
24
|
+
# deliveries when multiple publisher processes run concurrently.
|
|
25
|
+
class PostgresStrategy
|
|
26
|
+
def apply(scope)
|
|
27
|
+
scope.lock("FOR UPDATE SKIP LOCKED")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
# Reads unpublished events from the outbox and dispatches each through the
|
|
3
|
+
# injected router. Handles retries and dead-lettering on failure.
|
|
4
|
+
#
|
|
5
|
+
# Fires +ActiveSupport::Notifications+ for published events, dead letters,
|
|
6
|
+
# and batch completion.
|
|
7
|
+
class OutboxPublisher
|
|
8
|
+
# @param router [#route] dispatches each drained event to its destination
|
|
9
|
+
# @param batch_size [Integer, nil] max events per batch (nil for unlimited)
|
|
10
|
+
# @param max_attempts [Integer, nil] max attempts before dead-lettering
|
|
11
|
+
def initialize(router:, batch_size: nil, max_attempts: nil, locking_strategy: nil)
|
|
12
|
+
@router = router
|
|
13
|
+
@batch_size = batch_size
|
|
14
|
+
@max_attempts = max_attempts
|
|
15
|
+
@locking_strategy = locking_strategy || LockingStrategy.for_current_adapter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Fetches and publishes a batch of unpublished events.
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
def call
|
|
22
|
+
OutboxEvent.transaction do
|
|
23
|
+
events = batch
|
|
24
|
+
events.each do |event|
|
|
25
|
+
publish_event(event)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ActiveSupport::Notifications.instrument("event_engine.publish_batch", {
|
|
29
|
+
count: events.size
|
|
30
|
+
})
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def batch
|
|
37
|
+
scope = OutboxEvent.unpublished
|
|
38
|
+
.active
|
|
39
|
+
.ordered
|
|
40
|
+
|
|
41
|
+
scope = scope.retryable(@max_attempts) if @max_attempts
|
|
42
|
+
scope = scope.limit(@batch_size) if @batch_size
|
|
43
|
+
scope = @locking_strategy.apply(scope)
|
|
44
|
+
|
|
45
|
+
scope.to_a
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def publish_event(event)
|
|
49
|
+
@router.route(event)
|
|
50
|
+
event.update!(published_at: Time.current)
|
|
51
|
+
|
|
52
|
+
ActiveSupport::Notifications.instrument("event_engine.event_published", {
|
|
53
|
+
event_name: event.event_name,
|
|
54
|
+
event_version: event.event_version,
|
|
55
|
+
event_id: event.id
|
|
56
|
+
})
|
|
57
|
+
rescue => e
|
|
58
|
+
handle_failure(event, e)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_failure(event, error)
|
|
62
|
+
event.increment!(:attempts)
|
|
63
|
+
event.update!(
|
|
64
|
+
last_error_message: error.message.truncate(10_000),
|
|
65
|
+
last_error_class: error.class.name
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return unless @max_attempts
|
|
69
|
+
return unless event.attempts >= @max_attempts
|
|
70
|
+
|
|
71
|
+
event.update!(dead_lettered_at: Time.current)
|
|
72
|
+
|
|
73
|
+
ActiveSupport::Notifications.instrument("event_engine.event_dead_lettered", {
|
|
74
|
+
event_name: event.event_name,
|
|
75
|
+
event_version: event.event_version,
|
|
76
|
+
event_id: event.id,
|
|
77
|
+
attempts: event.attempts,
|
|
78
|
+
error_message: error.message,
|
|
79
|
+
error_class: error.class.name
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
Rails.logger.error(
|
|
83
|
+
"[EventEngine] Dead-lettered event: event_id=#{event.id}, " \
|
|
84
|
+
"event_name=#{event.event_name}, attempts=#{event.attempts}, " \
|
|
85
|
+
"error=#{error.message}"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
# Routes a drained outbox event to its destination based on +process_type+.
|
|
3
|
+
class OutboxRouter
|
|
4
|
+
# Raised when routing an event whose process_type has no supported destination.
|
|
5
|
+
class UnsupportedProcessTypeError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when a :broker event is routed but no transport is configured.
|
|
8
|
+
class MissingTransportError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# @param transport [#publish] the broker transport used for :broker events
|
|
11
|
+
def initialize(transport:)
|
|
12
|
+
@transport = transport
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Dispatches a drained event to its process_type's destination.
|
|
16
|
+
#
|
|
17
|
+
# @param event [OutboxEvent] the drained event
|
|
18
|
+
# @return [void]
|
|
19
|
+
def route(event)
|
|
20
|
+
case process_type_for(event)
|
|
21
|
+
when :durable then notify_subscribers(event)
|
|
22
|
+
when :broker then deliver_to_broker(event)
|
|
23
|
+
when :sourced then raise UnsupportedProcessTypeError, "process_type :sourced (event sourcing) is not supported"
|
|
24
|
+
else publish(event)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def process_type_for(event)
|
|
31
|
+
event.process_type&.to_sym
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def notify_subscribers(record)
|
|
35
|
+
event = Event.from(record)
|
|
36
|
+
EventEngine::Subscribers::Registry.subscribers_for(event.event_name).each do |subscriber|
|
|
37
|
+
subscriber.new.handle(event)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deliver_to_broker(event)
|
|
42
|
+
if transport_missing?
|
|
43
|
+
raise MissingTransportError,
|
|
44
|
+
"process_type :broker event '#{event.event_name}' requires a transport, but none is configured"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
publish(event)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def transport_missing?
|
|
51
|
+
@transport.nil? || (@transport.respond_to?(:null?) && @transport.null?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def publish(event)
|
|
55
|
+
@transport.publish(event)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Transports
|
|
3
|
+
# Stores published events in memory. Useful for development and testing.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# transport = EventEngine::Transports::InMemoryTransport.new
|
|
7
|
+
# transport.publish(event)
|
|
8
|
+
# transport.events #=> [event]
|
|
9
|
+
class InMemoryTransport
|
|
10
|
+
# @return [Array<OutboxEvent>] all published events
|
|
11
|
+
attr_reader :events
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@events = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Stores the event in the in-memory array.
|
|
18
|
+
#
|
|
19
|
+
# @param event [OutboxEvent]
|
|
20
|
+
# @return [true]
|
|
21
|
+
def publish(event)
|
|
22
|
+
@events << event
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Transports
|
|
3
|
+
# Publishes events to Kafka topics via an injected producer.
|
|
4
|
+
# Topics are named +events.{event_name}+ by default.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# producer = EventEngine::KafkaProducer.new(client: kafka_client)
|
|
8
|
+
# transport = EventEngine::Transports::Kafka.new(producer: producer)
|
|
9
|
+
class Kafka
|
|
10
|
+
# @param producer [KafkaProducer] a producer that responds to +#publish(topic, payload)+
|
|
11
|
+
def initialize(producer:)
|
|
12
|
+
@producer = producer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Publishes the event to a Kafka topic.
|
|
16
|
+
#
|
|
17
|
+
# @param event [OutboxEvent]
|
|
18
|
+
# @return [Object] the producer's return value
|
|
19
|
+
def publish(event)
|
|
20
|
+
@producer.publish(topic_for(event), payload_for(event))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def topic_for(event)
|
|
26
|
+
"events.#{event.event_name}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def payload_for(event)
|
|
30
|
+
{
|
|
31
|
+
event_name: event.event_name,
|
|
32
|
+
event_type: event.event_type,
|
|
33
|
+
event_version: event.event_version,
|
|
34
|
+
idempotency_key: event.idempotency_key,
|
|
35
|
+
payload: event.payload,
|
|
36
|
+
metadata: event.metadata,
|
|
37
|
+
occurred_at: event.occurred_at,
|
|
38
|
+
aggregate_type: event.aggregate_type,
|
|
39
|
+
aggregate_id: event.aggregate_id,
|
|
40
|
+
aggregate_version: event.aggregate_version
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Transports
|
|
3
|
+
# Default transport that discards events with a warning log.
|
|
4
|
+
# Used when no transport is explicitly configured.
|
|
5
|
+
class NullTransport
|
|
6
|
+
# Logs a warning that the event was discarded.
|
|
7
|
+
#
|
|
8
|
+
# @param event [OutboxEvent]
|
|
9
|
+
# @return [true]
|
|
10
|
+
def publish(event)
|
|
11
|
+
logger.warn("[EventEngine::NullTransport] Event '#{event.event_name}' discarded. No transport configured.")
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Boolean] true, since this transport does not deliver anywhere
|
|
16
|
+
def null?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def logger
|
|
23
|
+
EventEngine::Delivery.configuration.logger
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "event_engine"
|
|
2
|
+
|
|
3
|
+
require "event_engine/delivery/configuration"
|
|
4
|
+
require "event_engine/delivery"
|
|
5
|
+
require "event_engine/transports/in_memory_transport"
|
|
6
|
+
require "event_engine/transports/kafka"
|
|
7
|
+
require "event_engine/transports/null_transport"
|
|
8
|
+
require "event_engine/kafka_producer"
|
|
9
|
+
require "event_engine/locking_strategy"
|
|
10
|
+
require "event_engine/outbox_writer"
|
|
11
|
+
require "event_engine/outbox_router"
|
|
12
|
+
require "event_engine/outbox_publisher"
|
|
13
|
+
require "event_engine/definition_transport_check"
|
|
14
|
+
require "event_engine/cloud/serializer"
|
|
15
|
+
require "event_engine/cloud/batch"
|
|
16
|
+
require "event_engine/cloud/api_client"
|
|
17
|
+
require "event_engine/cloud/subscribers"
|
|
18
|
+
require "event_engine/cloud/reporter"
|
|
19
|
+
require "event_engine/delivery/handler"
|
|
20
|
+
require "event_engine/delivery/engine"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "event_engine/subagents"
|
|
2
|
+
|
|
3
|
+
module EventEngine
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Install EventEngine (schema file, initializer, migrations)"
|
|
9
|
+
|
|
10
|
+
def install_migrations
|
|
11
|
+
rake "event_engine:install:migrations"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_event_schema
|
|
15
|
+
template "event_schema.rb", "db/event_schema.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_initializer
|
|
19
|
+
template "initializer.rb", "config/initializers/event_engine.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_subagents
|
|
23
|
+
say ""
|
|
24
|
+
say "Installing EventEngine Claude Code subagents...", :green
|
|
25
|
+
EventEngine::Subagents.names.each do |name|
|
|
26
|
+
create_file ".claude/agents/#{name}.md", EventEngine::Subagents.content_for(name), force: true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def print_next_steps
|
|
31
|
+
say <<~MSG
|
|
32
|
+
|
|
33
|
+
EventEngine installed.
|
|
34
|
+
|
|
35
|
+
Next steps:
|
|
36
|
+
1. Define events in app/event_definitions/
|
|
37
|
+
2. Run: bin/rails event_engine:schema:dump
|
|
38
|
+
3. Commit db/event_schema.rb
|
|
39
|
+
4. Configure transport in config/initializers/event_engine.rb
|
|
40
|
+
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
EventEngine.configure do |c|
|
|
2
|
+
# Delivery strategy (:inline or :active_job)
|
|
3
|
+
# c.delivery_adapter = :active_job
|
|
4
|
+
|
|
5
|
+
# Publisher behavior
|
|
6
|
+
# c.batch_size = 100
|
|
7
|
+
# c.max_attempts = 5
|
|
8
|
+
|
|
9
|
+
# Transport (required for publishing)
|
|
10
|
+
#
|
|
11
|
+
# kafka = Kafka.new(seed_brokers: ENV["KAFKA_BROKERS"])
|
|
12
|
+
# producer = EventEngine::KafkaProducer.new(client: kafka)
|
|
13
|
+
# c.transport = EventEngine::Transports::Kafka.new(producer: producer)
|
|
14
|
+
end
|