event_engine-delivery 0.1.0 → 0.2.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: 11a0ab2cbc92fee65fae05651dc9908743196fae615427bb0bfbcfab5b2c97d0
4
- data.tar.gz: 0104a00e5c2ca4434500ddd21690b08188cfe66fb6b298db34ace0276af8bd66
3
+ metadata.gz: 8855fa65e34aece1ee20813207c5092e0aae8cf133fabaed432e61fcb63cd75a
4
+ data.tar.gz: 369f3e3db3efd94a6879b15af7d4c22097814e015d3ee6317ab772cd242efb95
5
5
  SHA512:
6
- metadata.gz: 9fba2892fbf51bf6b50ce2cfd5b7dc585b71053e02a2e06ee83c7facff638171b829d58cbf04538545e0a4656d64b36ddabb3a2fd3e8ac38d7375bc98995f215
7
- data.tar.gz: 8066c109de6d3e75875ca276eff90e302a24f2c3eaf0695a9ef6cecf3c399414fc13899261d0d6568389e2398506ba7fb2c93d7cfa37caea0f955b45118fad8a
6
+ metadata.gz: b52d6548f25680b293362c4980e1244194b32f34fcbee376fc866cc3dc0e61962ad220d2b56465c712f35a47821a93fb358b184fea633532e88f677f30ff9bda
7
+ data.tar.gz: 85cf4fdf4762a80df6cdf465717757148c30e16cb49d3f02fb745fc268581bcf16eee62afb1432c4d70ef860bc2c765b16409b446da987473733262364f4cdae
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ All notable changes to this project are documented here, following
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-10-23
10
+
11
+ ### Changed
12
+ - Delivery registers itself as the processor for the `durable` and `broker` process
13
+ types, in place of a handler registered for every event. An event reaches the
14
+ outbox only when the host's rules route it to `durable` or `broker`.
15
+ - A `durable` event is delivered to its subscribers when no transport is configured.
16
+
17
+ ## [0.1.0] - 2026-09-21
18
+
9
19
  ### Added
10
20
  - Initial gem scaffold: mountable `EventEngine::Delivery` Rails engine depending on
11
21
  `event_engine`.
data/README.md CHANGED
@@ -4,7 +4,7 @@ The **delivery layer** for [EventEngine](https://github.com/tylercschneider/even
4
4
 
5
5
  `event_engine` (the core) declares events with a schema-first DSL, compiles them to
6
6
  a committed schema, and **dispatches** built events to registered handlers. It does
7
- not deliver anything on its own. `event_engine-delivery` is the handler that takes an
7
+ not deliver anything on its own. `event_engine-delivery` is the processor that takes an
8
8
  emitted event and **gets it delivered reliably**:
9
9
 
10
10
  - the durability **level ladder** (1 sync → 2 job → 3 outbox → 4 broker),
@@ -21,7 +21,7 @@ when you need durable, reliable delivery.
21
21
  > via copy-then-subtract. Most of its classes still live under the top-level
22
22
  > `EventEngine::` namespace (e.g. `EventEngine::Transports::Kafka`,
23
23
  > `EventEngine::OutboxEvent`, `EventEngine::OutboxPublisher`). The delivery-specific
24
- > entry points are `EventEngine::Delivery` (config, engine, handler). The examples
24
+ > entry points are `EventEngine::Delivery` (config, engine, processor). The examples
25
25
  > below use the namespaces as they exist today.
26
26
 
27
27
  ---
@@ -87,54 +87,37 @@ end
87
87
 
88
88
  ## How it hooks into the core
89
89
 
90
- At Rails boot, the delivery engine registers a single handler with the core, for all
91
- levels:
90
+ At Rails boot, the delivery engine registers itself with the core as the processor
91
+ for the `durable` and `broker` process types:
92
92
 
93
93
  ```ruby
94
94
  # lib/event_engine/delivery/engine.rb
95
- initializer "event_engine.delivery.register_handler" do
95
+ initializer "event_engine.delivery.register_processor" do
96
96
  config.after_initialize do
97
- EventEngine.register_handler(Handler.new, levels: :all)
97
+ processor = Processor.new
98
+ Processor::PROCESS_TYPES.each { |process_type| EventEngine.register_processor(process_type, processor) }
98
99
  Engine.send(:start_cloud_reporter!)
99
100
  end
100
101
  end
101
102
  ```
102
103
 
103
- From then on, every `EventEngine.<event>` call dispatches into
104
- `EventEngine::Delivery::Handler#call`, which routes by `event_level`.
104
+ The host's rules file decides which events take those roads, for example
105
+ `quote_signed: durable`. `inline` and `background` events are processed by
106
+ `event_engine-subscribers` and never reach this gem.
105
107
 
106
108
  ---
107
109
 
108
- ## Level routing
110
+ ## Process type routing
109
111
 
110
- `EventEngine::Delivery::Handler` is the brain. It maps each event's level to a
111
- delivery strategy:
112
+ `EventEngine::Delivery::Processor#call` writes the event to the outbox, fires
113
+ `event_engine.event_emitted`, and triggers publishing per the configured
114
+ [delivery adapter](#delivery-adapters). When the outbox drains,
115
+ `EventEngine::OutboxRouter` routes by process type:
112
116
 
113
- ```ruby
114
- def call(event)
115
- case event.event_level
116
- when 1 then dispatch_synchronously(event) # invoke subscribers now, in-process
117
- when 2 then dispatch_in_background(event) # enqueue DispatchSubscribersJob
118
- else write_and_publish(event) # 3, 4, and nil → outbox + publish
119
- end
120
- end
121
- ```
122
-
123
- - **Level 1** — calls every `EventEngine::Subscriber` registered for the event,
124
- synchronously, in the caller's stack.
125
- - **Level 2** — enqueues `DispatchSubscribersJob`, which invokes the same subscribers
126
- in a background job.
127
- - **Levels 3+ (and `nil`)** — writes an `OutboxEvent` row, fires
128
- `event_engine.event_emitted`, and triggers publishing per the configured
129
- [delivery adapter](#delivery-adapters). When the outbox drains,
130
- `EventEngine::OutboxRouter` differentiates:
131
- - **level 3** → invoke in-app subscribers,
132
- - **level 4** → publish to the configured **transport** (raises
133
- `MissingTransportError` if none/Null),
134
- - **level 5** → raises `UnsupportedLevelError` (reserved, unsupported).
135
-
136
- > **`nil` levels fall into the outbox path** (the `else` branch). If you didn't set
137
- > `event_level` on a definition, its events behave like level 3+. Set it explicitly.
117
+ - **durable** → invokes the event's in-app subscribers. No transport is needed.
118
+ - **broker** → publishes to the configured **transport**, raising
119
+ `MissingTransportError` if none is configured.
120
+ - **sourced** → raises `UnsupportedProcessTypeError`.
138
121
 
139
122
  ---
140
123
 
@@ -470,9 +453,9 @@ app.
470
453
 
471
454
  ## Customization recipes
472
455
 
473
- **Run delivery and a durable log together.** Add `event_engine-store`. Both register
474
- handlers at `levels: :all`; every event is delivered *and* recorded. Order is the
475
- registration order at boot.
456
+ **Run delivery and a durable log together.** Add `event_engine-store`. Delivery
457
+ processes `durable` and `broker` events, and the store's handlers record every
458
+ event whatever its process type.
476
459
 
477
460
  **Pause delivery during maintenance.** Set `delivery_adapter = :manual`; events keep
478
461
  landing in the outbox safely and you drain them with `OutboxPublisher` when ready.
data/Rakefile CHANGED
@@ -7,3 +7,5 @@ require "bundler/gem_tasks"
7
7
 
8
8
  task test: "app:test"
9
9
  task default: :test
10
+
11
+ require "the_local/rake"
@@ -3,9 +3,10 @@ module EventEngine
3
3
  class Engine < ::Rails::Engine
4
4
  engine_name "event_engine_delivery"
5
5
 
6
- initializer "event_engine.delivery.register_handler" do
6
+ initializer "event_engine.delivery.register_processor" do
7
7
  config.after_initialize do
8
- EventEngine.register_handler(Handler.new, process_types: :all)
8
+ processor = Processor.new
9
+ Processor::PROCESS_TYPES.each { |process_type| EventEngine.register_processor(process_type, processor) }
9
10
  Engine.send(:start_cloud_reporter!)
10
11
  end
11
12
  end
@@ -0,0 +1,30 @@
1
+ module EventEngine
2
+ module Delivery
3
+ class Processor
4
+ PROCESS_TYPES = %i[durable broker].freeze
5
+
6
+ def call(event)
7
+ outbox_event = OutboxWriter.write(event.to_h)
8
+
9
+ ActiveSupport::Notifications.instrument("event_engine.event_emitted", {
10
+ event_name: outbox_event.event_name,
11
+ event_version: outbox_event.event_version,
12
+ event_id: outbox_event.id,
13
+ idempotency_key: outbox_event.idempotency_key,
14
+ aggregate_type: outbox_event.aggregate_type,
15
+ aggregate_id: outbox_event.aggregate_id,
16
+ aggregate_version: outbox_event.aggregate_version
17
+ })
18
+
19
+ Delivery.enqueue do
20
+ OutboxPublisher.new(
21
+ router: OutboxRouter.new(transport: Delivery.configuration.transport),
22
+ batch_size: Delivery.configuration.batch_size
23
+ ).call
24
+ end
25
+
26
+ outbox_event
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  module EventEngine
2
2
  module Delivery
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -16,5 +16,5 @@ require "event_engine/cloud/batch"
16
16
  require "event_engine/cloud/api_client"
17
17
  require "event_engine/cloud/subscribers"
18
18
  require "event_engine/cloud/reporter"
19
- require "event_engine/delivery/handler"
19
+ require "event_engine/delivery/processor"
20
20
  require "event_engine/delivery/engine"
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: event_engine-delivery-develop
3
+ description: Use PROACTIVELY for testing durable and broker events with the in-memory transport, scheduling outbox publishing with EventEngine::PublishOutboxEventsJob, scheduling outbox cleanup with EventEngine::OutboxCleanupJob or the cleanup task, and listing or retrying dead-lettered events — MUST BE USED instead of hand-writing test transports, outbox polling jobs, outbox delete queries, or console code that resets failed events.
4
+ tools: Read, Write, Edit, Grep
5
+ scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
6
+ ---
7
+
8
+ This local writes the host code that tests, schedules and repairs outbox
9
+ delivery. It follows these steps exactly, and where a step names a decision it
10
+ asks the developer and does not pick.
11
+
12
+ ## What event_engine-delivery is
13
+
14
+ The delivery layer for EventEngine. Every `durable` and `broker` event is written
15
+ to an outbox table, and a publish run later reads unpublished rows in order,
16
+ hands `durable` events to their in-app subscribers and sends `broker` events
17
+ through the configured transport. A row that fails to publish counts an attempt,
18
+ and a publish run with an attempt limit dead letters it once the limit is
19
+ reached. Use this local once the gem is installed and the work is testing that
20
+ events leave the app, running the publish or cleanup work on a schedule, or
21
+ dealing with events that failed.
22
+
23
+ ## Interface
24
+
25
+ - `EventEngine::Transports::InMemoryTransport` — a transport for tests and
26
+ development. `InMemoryTransport.new` builds it, `publish(event)` stores the
27
+ event, and `events` returns every stored outbox record in the order published.
28
+ - `EventEngine::PublishOutboxEventsJob` — an Active Job on the `default` queue
29
+ that takes no arguments and runs one publish run of up to `batch_size`
30
+ unpublished, non-dead-lettered rows with fewer than `max_attempts` attempts. It
31
+ dead letters a row whose attempts reach `max_attempts`, and it raises when the
32
+ configured transport is `nil`.
33
+ - `EventEngine::OutboxCleanupJob` — an Active Job on the `default` queue that
34
+ takes no arguments and deletes published, non-dead-lettered rows published
35
+ more than `retention_period` ago. It does nothing when `retention_period` is
36
+ unset.
37
+ - `bin/rails event_engine:outbox:cleanup` — the same deletion as
38
+ `OutboxCleanupJob`, run once from the shell, printing how many rows it deleted
39
+ or why it deleted none.
40
+ - `bin/rails event_engine:dead_letters:list` — prints every dead-lettered row,
41
+ oldest first, with its id, event name, attempts and the time it was dead
42
+ lettered.
43
+ - `bin/rails event_engine:dead_letters:retry[EVENT_ID]` — resets one
44
+ dead-lettered row to zero attempts with no error and no dead-letter mark, so
45
+ the next publish run picks it up again.
46
+ - `bin/rails event_engine:dead_letters:retry:all` — the same reset for every
47
+ dead-lettered row.
48
+
49
+ ## How to use it
50
+
51
+ ### Test that events are delivered
52
+
53
+ 1. Confirm the event under test is `durable` or `broker`. Other process types
54
+ never reach the outbox. Defining the event and emitting it belong to the
55
+ event_engine-develop local.
56
+ 2. In the test setup, keep the current transport and swap in the in-memory one:
57
+ ```ruby
58
+ @previous_transport = EventEngine::Delivery.configuration.transport
59
+ @transport = EventEngine::Transports::InMemoryTransport.new
60
+ EventEngine::Delivery.configuration.transport = @transport
61
+ ```
62
+ In teardown, put it back, since the configuration is shared by the whole
63
+ process:
64
+ ```ruby
65
+ EventEngine::Delivery.configuration.transport = @previous_transport
66
+ ```
67
+ 3. Emit the event, then run a publish run directly so the assertion does not
68
+ depend on the delivery adapter or on the test's transaction committing:
69
+ ```ruby
70
+ EventEngine::PublishOutboxEventsJob.perform_now
71
+ ```
72
+ 4. For a `broker` event, assert on what reached the transport:
73
+ ```ruby
74
+ assert_equal ["order_placed"], @transport.events.map(&:event_name)
75
+ ```
76
+ Each entry answers `event_name`, `event_version`, `payload`, `metadata`,
77
+ `idempotency_key` and `occurred_at`.
78
+ 5. For a `durable` event, assert on the subscriber's effect instead. `durable`
79
+ events go to in-app subscribers and never reach the transport, so
80
+ `@transport.events` stays empty for them.
81
+
82
+ ### Schedule outbox publishing
83
+
84
+ 1. Read `delivery_adapter` from the host's delivery initializer.
85
+ - `:manual` — nothing publishes on its own, so `PublishOutboxEventsJob` must
86
+ be scheduled.
87
+ - `:active_job` — each emit already enqueues the job. A schedule is still
88
+ needed for rows that failed, because nothing else re-runs them until the
89
+ next emit.
90
+ - `:inline` — each emit publishes right after its transaction commits, and
91
+ that publish applies no attempt limit, so nothing is ever dead lettered by
92
+ it. A schedule is needed only if the developer wants failed rows dead
93
+ lettered after `max_attempts`.
94
+ Ask the developer whether to add a schedule, and how often it runs.
95
+ 2. Ask the developer which scheduler the host uses, for example Solid Queue
96
+ recurring tasks, sidekiq-cron, GoodJob cron or system cron. Look for an
97
+ existing one in the host before asking. This gem ships no scheduler.
98
+ 3. Add one recurring entry that enqueues `EventEngine::PublishOutboxEventsJob`
99
+ with no arguments, in that scheduler's own format. For system cron, call it
100
+ through `bin/rails runner "EventEngine::PublishOutboxEventsJob.perform_now"`.
101
+ 4. Confirm the configured transport is not `nil`. The job raises when it is.
102
+
103
+ ### Schedule outbox cleanup
104
+
105
+ 1. Read `retention_period` from the host's delivery initializer. If it is unset, cleanup deletes nothing. Setting it belongs to the
106
+ event_engine-delivery-install local, so ask the developer whether to set one
107
+ there first, and stop here if they decline.
108
+ 2. Ask the developer how often cleanup runs.
109
+ 3. Add one recurring entry to the host's scheduler that enqueues
110
+ `EventEngine::OutboxCleanupJob` with no arguments. For system cron, run
111
+ `bin/rails event_engine:outbox:cleanup` instead, which also prints what it
112
+ deleted.
113
+ 4. To clean up once by hand, run:
114
+ ```sh
115
+ bin/rails event_engine:outbox:cleanup
116
+ ```
117
+
118
+ ### Handle dead-lettered events
119
+
120
+ 1. List them:
121
+ ```sh
122
+ bin/rails event_engine:dead_letters:list
123
+ ```
124
+ 2. Report the list to the developer and ask whether the cause of the failure is
125
+ fixed. A retried row that fails again is dead lettered again after another
126
+ `max_attempts` attempts.
127
+ 3. Ask the developer whether to retry one row or all of them.
128
+ - One row, quoting the brackets so the shell does not expand them:
129
+ ```sh
130
+ bin/rails "event_engine:dead_letters:retry[42]"
131
+ ```
132
+ - All rows:
133
+ ```sh
134
+ bin/rails event_engine:dead_letters:retry:all
135
+ ```
136
+ 4. Retrying does not publish. The row is sent by the next publish run, which is
137
+ the next scheduled `PublishOutboxEventsJob` or the next emitted event under
138
+ the `:inline` or `:active_job` adapter. To send it at once, run:
139
+ ```sh
140
+ bin/rails runner "EventEngine::PublishOutboxEventsJob.perform_now"
141
+ ```
142
+
143
+ ## Conventions
144
+
145
+ - Never write to the outbox table, delete from it, or reset its rows by hand.
146
+ Publishing goes through `PublishOutboxEventsJob`, deletion through
147
+ `OutboxCleanupJob` or the cleanup task, and resets through the retry tasks.
148
+ - Cleanup never deletes an unpublished row or a dead-lettered row.
149
+ - A dead-lettered row stays in the outbox until it is retried.
150
+ - Never write a test transport of your own. Use `InMemoryTransport`, and restore
151
+ the previous transport after each test.
152
+ - Out of scope: adding the gem, the outbox migrations, every delivery
153
+ initializer setting including `retention_period`, and the Kafka transport and
154
+ producer. Those belong to the
155
+ event_engine-delivery-install local. Defining events, choosing their process
156
+ type and emitting them belong to EventEngine itself.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: event_engine-delivery-info
3
+ description: Use to learn what event_engine-delivery offers — the transactional outbox, durable and broker events, transports, retries, dead letters, and outbox retention.
4
+ tools: Read
5
+ scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
6
+ ---
7
+
8
+ This local explains event_engine-delivery and makes no changes.
9
+
10
+ ## What event_engine-delivery is
11
+
12
+ event_engine-delivery is the delivery layer for EventEngine. EventEngine defines
13
+ and emits events, and each event carries a process type. This gem handles the
14
+ two process types that must not be lost: `durable` and `broker`. It writes each
15
+ such event to an outbox table in the same database transaction as the change
16
+ that caused it. It then publishes the event once that transaction commits.
17
+
18
+ Reach for it when an event has to survive a crash or a rolled-back request, or
19
+ when an event has to leave the app through a message broker. Events with the
20
+ `inline` or `background` process type are not handled here. event_engine-subscribers
21
+ runs those.
22
+
23
+ ## Interface
24
+
25
+ This local declares no commands. The other two locals own the whole surface:
26
+
27
+ - **event_engine-delivery-install** owns adding the gem to an app. That covers
28
+ the outbox migrations, the initializer settings, and connecting a real broker
29
+ transport.
30
+ - **event_engine-delivery-develop** owns working with the gem once it is
31
+ installed. That covers the test transport, the jobs that publish and clean up
32
+ the outbox, and the tasks that list and retry dead-lettered events.
33
+
34
+ ## How to use it
35
+
36
+ - The gem is not in the app yet, or the app needs a broker connection: use the
37
+ install local.
38
+ - The gem is installed and you are writing tests, scheduling outbox work, or
39
+ handling failed events: use the develop local.
40
+ - You are defining an event or choosing its process type: that belongs to
41
+ EventEngine itself, not this gem.
42
+
43
+ ## Conventions
44
+
45
+ - **Outbox** — the table every durable and broker event is written to before it
46
+ is published. A row stays there after publishing until retention removes it.
47
+ - **`durable`** — an event written to the outbox and then handed to its
48
+ subscribers inside the app. It needs no broker.
49
+ - **`broker`** — an event written to the outbox and then published through the
50
+ transport. Publishing raises an error when no real transport is configured.
51
+ - **`sourced`** — a process type for event sourcing. This gem does not support
52
+ it and raises an error when one is routed.
53
+ - **Transport** — the object that sends a broker event out of the app. Any
54
+ object that responds to `publish(event)` qualifies. The default transport
55
+ publishes nothing. A Kafka transport ships with the gem and names topics
56
+ `events.<event_name>`.
57
+ - **Delivery adapter** — when the outbox gets published. `inline` publishes
58
+ right after the transaction commits. `active_job` publishes from a background
59
+ job and requires a real transport. `manual` publishes only when something
60
+ calls for it explicitly.
61
+ - **Batch size** — how many outbox rows one publish run reads. The default is
62
+ 100.
63
+ - **Max attempts** — how many failed publishes an event gets before it is dead
64
+ lettered. The default is 5. It applies when the outbox is published by the
65
+ background job. An `inline` publish applies no attempt limit.
66
+ - **Dead letter** — an event that ran out of attempts. It stays in the outbox,
67
+ marked dead lettered, until someone retries it.
68
+ - **Retention period** — how long a published event stays in the outbox. It is
69
+ unset by default, and while it is unset nothing is cleaned up.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: event_engine-delivery-install
3
+ description: Use to hook event_engine-delivery into a project — copying the outbox migrations, writing the EventEngine::Delivery.configure initializer, and connecting Kafka through EventEngine::Transports::Kafka and EventEngine::KafkaProducer.
4
+ tools: Bash, Read, Edit
5
+ scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
6
+ ---
7
+
8
+ This local follows these steps exactly and invents none. Where a step names a
9
+ decision, it asks the developer and does not pick.
10
+
11
+ ## What event_engine-delivery is
12
+
13
+ The delivery layer for EventEngine: it writes `durable` and `broker` events to an
14
+ outbox table and publishes them after the transaction commits. Hook it in when an
15
+ app emits events that must not be lost or must leave the app through a broker.
16
+
17
+ ## Interface
18
+
19
+ - `bin/rails event_engine_delivery:install:migrations` — copies the outbox table
20
+ migrations into the host's `db/migrate/`.
21
+ - `EventEngine::Delivery.configure` — the block in the host initializer that sets
22
+ the delivery adapter, transport, batch size, max attempts, retention period,
23
+ logger and cloud reporting.
24
+ - `EventEngine::Transports::Kafka` — the transport that publishes each broker
25
+ event to the Kafka topic `events.<event_name>`. Built with
26
+ `EventEngine::Transports::Kafka.new(producer: producer)`.
27
+ - `EventEngine::KafkaProducer` — wraps a Kafka client for that transport. Built
28
+ with `EventEngine::KafkaProducer.new(client: client)`. It calls
29
+ `client.produce(json_string, topic: topic)`, so the client must answer that
30
+ call.
31
+
32
+ ## How to use it
33
+
34
+ 1. Confirm EventEngine itself is installed in the host. If it is not, stop and
35
+ use the event_engine-install local first.
36
+ 2. Add the gem to the host `Gemfile` and install it:
37
+ ```ruby
38
+ gem "event_engine-delivery"
39
+ ```
40
+ ```sh
41
+ bundle install
42
+ ```
43
+ The gem brings in `event_engine-subscribers` and holds `json` below version 3.
44
+ If the host's lockfile already pins `json` 3 or later, report the conflict to
45
+ the developer and stop.
46
+ 3. Copy and run the outbox migrations:
47
+ ```sh
48
+ bin/rails event_engine_delivery:install:migrations
49
+ bin/rails db:migrate
50
+ ```
51
+ This creates the `event_engine_outbox_events` table and updates
52
+ `db/schema.rb`.
53
+ 4. Ask the developer which delivery adapter to use:
54
+ - `:inline` (the default) — publishes right after the transaction commits, in
55
+ the same process.
56
+ - `:active_job` — publishes from a background job, which needs a working
57
+ Active Job backend in the host and a real transport.
58
+ - `:manual` — publishes only when something calls for it explicitly.
59
+ 5. Ask the developer whether the app emits `broker` events. If it does, it needs
60
+ a real transport, and Kafka is the one this gem ships. Ask which Kafka client
61
+ the host uses, and confirm that client answers
62
+ `produce(json_string, topic: topic)`. This gem does not add a Kafka client
63
+ gem, so the developer adds and configures the client. If the client does not
64
+ answer that call, report it and stop rather than writing an adapter.
65
+ 6. Create `config/initializers/event_engine_delivery.rb`:
66
+ ```ruby
67
+ EventEngine::Delivery.configure do |config|
68
+ config.delivery_adapter = :inline
69
+
70
+ # Only when the app emits broker events:
71
+ # producer = EventEngine::KafkaProducer.new(client: kafka_client)
72
+ # config.transport = EventEngine::Transports::Kafka.new(producer: producer)
73
+ end
74
+ ```
75
+ Set `delivery_adapter` to the developer's answer from step 4. Uncomment the
76
+ transport lines when step 5 found broker events, with `kafka_client` replaced
77
+ by the host's client.
78
+ 7. Ask the developer for each remaining setting, and write only the ones that
79
+ differ from the default:
80
+ - `config.batch_size` — outbox rows read per publish run. Default `100`.
81
+ - `config.max_attempts` — failed publishes before an event is dead lettered.
82
+ Default `5`.
83
+ - `config.retention_period` — how long published events stay in the outbox,
84
+ for example `30.days`. Default unset, and while unset nothing is removed.
85
+ - `config.logger` — default `Rails.logger`.
86
+ - `config.cloud_api_key`, `config.cloud_environment`, `config.cloud_app_name`
87
+ — setting an API key turns on reporting to EventEngine Cloud at boot. Ask
88
+ before setting it, and read the key from credentials or the environment,
89
+ never a literal.
90
+ 8. Boot the app and check the configuration:
91
+ ```sh
92
+ bin/rails runner "EventEngine::Delivery.configuration.validate!"
93
+ ```
94
+ It raises when the adapter is not one of the three, when `:active_job` has no
95
+ real transport, when the transport does not answer `publish`, or when
96
+ `batch_size` or `max_attempts` is not a positive integer. The app does not run
97
+ this check on its own at boot.
98
+
99
+ ## Conventions
100
+
101
+ - The gem registers itself with EventEngine as the processor for `durable` and
102
+ `broker` when the app boots. No routes, mount or processor registration goes in
103
+ the host, and only events the host's rules route to `durable` or `broker` reach
104
+ the outbox.
105
+ - Without a real transport, a `broker` event is still written to the outbox, and
106
+ publishing it raises. `durable` events need no transport.
107
+ - When the gem is upgraded, run `bin/rails event_engine_delivery:install:migrations`
108
+ and `bin/rails db:migrate` again to pick up new outbox migrations.
109
+ - Out of scope: the test transport, scheduling outbox publishing or cleanup, and
110
+ listing or retrying dead-lettered events. Those belong to the
111
+ event_engine-delivery-develop local. Defining events and choosing their process
112
+ type belong to EventEngine itself.
@@ -0,0 +1,28 @@
1
+ scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
2
+
3
+ install:
4
+ - bin/rails event_engine_delivery:install:migrations
5
+ - EventEngine::Delivery.configure
6
+ - EventEngine::Transports::Kafka
7
+ - EventEngine::KafkaProducer
8
+
9
+ develop:
10
+ - EventEngine::Transports::InMemoryTransport
11
+ - EventEngine::PublishOutboxEventsJob
12
+ - EventEngine::OutboxCleanupJob
13
+ - bin/rails event_engine:outbox:cleanup
14
+ - bin/rails event_engine:dead_letters:list
15
+ - bin/rails event_engine:dead_letters:retry
16
+ - bin/rails event_engine:dead_letters:retry:all
17
+
18
+ sources:
19
+ - lib/event_engine/delivery.rb
20
+ - lib/event_engine/delivery/configuration.rb
21
+ - lib/event_engine/delivery/handler.rb
22
+ - lib/event_engine/transports/kafka.rb
23
+ - lib/event_engine/kafka_producer.rb
24
+ - lib/event_engine/transports/in_memory_transport.rb
25
+ - app/jobs/event_engine/publish_outbox_events_job.rb
26
+ - app/jobs/event_engine/outbox_cleanup_job.rb
27
+ - lib/tasks/event_engine_outbox.rake
28
+ - lib/tasks/event_engine_dead_letters.rake
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: event_engine-delivery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tylercschneider
@@ -135,7 +135,7 @@ files:
135
135
  - lib/event_engine/delivery.rb
136
136
  - lib/event_engine/delivery/configuration.rb
137
137
  - lib/event_engine/delivery/engine.rb
138
- - lib/event_engine/delivery/handler.rb
138
+ - lib/event_engine/delivery/processor.rb
139
139
  - lib/event_engine/delivery/version.rb
140
140
  - lib/event_engine/kafka_producer.rb
141
141
  - lib/event_engine/locking_strategy.rb
@@ -153,6 +153,10 @@ files:
153
153
  - lib/tasks/event_engine_schema.rake
154
154
  - lib/tasks/event_engine_schema_check.rake
155
155
  - lib/tasks/event_engine_tasks.rake
156
+ - the_local/agents/event_engine-delivery-develop.md
157
+ - the_local/agents/event_engine-delivery-info.md
158
+ - the_local/agents/event_engine-delivery-install.md
159
+ - the_local/interface.yml
156
160
  homepage: https://github.com/tylercschneider/event_engine-delivery
157
161
  licenses:
158
162
  - MIT
@@ -1,52 +0,0 @@
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