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,123 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
# Represents an event persisted in the outbox table. Events are written here
|
|
3
|
+
# before being published through a transport, implementing the outbox pattern
|
|
4
|
+
# for reliable delivery.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [rw] event_name
|
|
7
|
+
# @return [String] the event name (e.g. "cow_fed")
|
|
8
|
+
# @!attribute [rw] event_type
|
|
9
|
+
# @return [String] the event type (e.g. "domain")
|
|
10
|
+
# @!attribute [rw] event_version
|
|
11
|
+
# @return [Integer] the schema version
|
|
12
|
+
# @!attribute [rw] payload
|
|
13
|
+
# @return [Hash] the event payload data
|
|
14
|
+
# @!attribute [rw] metadata
|
|
15
|
+
# @return [Hash, nil] optional contextual metadata
|
|
16
|
+
# @!attribute [rw] idempotency_key
|
|
17
|
+
# @return [String, nil] unique key for deduplication
|
|
18
|
+
# @!attribute [rw] attempts
|
|
19
|
+
# @return [Integer] number of publish attempts
|
|
20
|
+
# @!attribute [rw] published_at
|
|
21
|
+
# @return [Time, nil] when the event was successfully published
|
|
22
|
+
# @!attribute [rw] dead_lettered_at
|
|
23
|
+
# @return [Time, nil] when the event was dead-lettered
|
|
24
|
+
# @!attribute [rw] occurred_at
|
|
25
|
+
# @return [Time] when the event occurred
|
|
26
|
+
class OutboxEvent < ApplicationRecord
|
|
27
|
+
self.table_name = "event_engine_outbox_events"
|
|
28
|
+
|
|
29
|
+
attr_readonly :event_name, :event_type, :event_version, :payload,
|
|
30
|
+
:metadata, :occurred_at, :idempotency_key
|
|
31
|
+
|
|
32
|
+
validates :event_name, presence: true
|
|
33
|
+
validates :event_type, presence: true
|
|
34
|
+
validates :payload, presence: true
|
|
35
|
+
validates :idempotency_key, uniqueness: true, allow_nil: true
|
|
36
|
+
|
|
37
|
+
# @!scope class
|
|
38
|
+
# @!method active
|
|
39
|
+
# Events that have not been dead-lettered.
|
|
40
|
+
# @return [ActiveRecord::Relation]
|
|
41
|
+
scope :active, -> { where(dead_lettered_at: nil) }
|
|
42
|
+
|
|
43
|
+
# @!scope class
|
|
44
|
+
# @!method dead_lettered
|
|
45
|
+
# Events that have been dead-lettered after exceeding max attempts.
|
|
46
|
+
# @return [ActiveRecord::Relation]
|
|
47
|
+
scope :dead_lettered, -> { where.not(dead_lettered_at: nil) }
|
|
48
|
+
|
|
49
|
+
# @!scope class
|
|
50
|
+
# @!method ordered
|
|
51
|
+
# Events ordered by creation time (FIFO).
|
|
52
|
+
# @return [ActiveRecord::Relation]
|
|
53
|
+
scope :ordered, -> { order(:created_at) }
|
|
54
|
+
|
|
55
|
+
# @!scope class
|
|
56
|
+
# @!method retryable(max_attempts)
|
|
57
|
+
# Events with fewer attempts than the maximum.
|
|
58
|
+
# @param max_attempts [Integer]
|
|
59
|
+
# @return [ActiveRecord::Relation]
|
|
60
|
+
scope :retryable, ->(max_attempts) { where("attempts < ?", max_attempts) }
|
|
61
|
+
|
|
62
|
+
# @!scope class
|
|
63
|
+
# @!method unpublished
|
|
64
|
+
# Events not yet published.
|
|
65
|
+
# @return [ActiveRecord::Relation]
|
|
66
|
+
scope :unpublished, -> { where(published_at: nil) }
|
|
67
|
+
|
|
68
|
+
# @!scope class
|
|
69
|
+
# @!method published_before(time)
|
|
70
|
+
# Events published before a given time (for cleanup).
|
|
71
|
+
# @param time [Time]
|
|
72
|
+
# @return [ActiveRecord::Relation]
|
|
73
|
+
scope :published_before, ->(time) { where("published_at < ?", time) }
|
|
74
|
+
|
|
75
|
+
# @!scope class
|
|
76
|
+
# @!method cleanable
|
|
77
|
+
# Published events that are not dead-lettered (safe to delete).
|
|
78
|
+
# @return [ActiveRecord::Relation]
|
|
79
|
+
scope :cleanable, -> { where.not(published_at: nil).where(dead_lettered_at: nil) }
|
|
80
|
+
|
|
81
|
+
scope :for_aggregate, ->(type, id) { where(aggregate_type: type, aggregate_id: id).ordered }
|
|
82
|
+
|
|
83
|
+
def self.next_aggregate_version(type, id)
|
|
84
|
+
max = where(aggregate_type: type, aggregate_id: id).maximum(:aggregate_version)
|
|
85
|
+
(max || 0) + 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Marks the event as dead-lettered.
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
def dead_letter!
|
|
92
|
+
update!(dead_lettered_at: Time.current)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether the event has been dead-lettered.
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def dead_lettered?
|
|
99
|
+
dead_lettered_at.present?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Resets the event for retry by clearing attempts and dead-letter status.
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
def retry!
|
|
106
|
+
update!(attempts: 0, dead_lettered_at: nil, last_error_message: nil, last_error_class: nil)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Increments the attempt counter.
|
|
110
|
+
#
|
|
111
|
+
# @return [void]
|
|
112
|
+
def increment_attempts!
|
|
113
|
+
update!(attempts: (attempts || 0) + 1)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Marks the event as successfully published.
|
|
117
|
+
#
|
|
118
|
+
# @return [void]
|
|
119
|
+
def mark_published!
|
|
120
|
+
update!(published_at: Time.current)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddPayloadToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
if connection.adapter_name.downcase.include?("postgres")
|
|
4
|
+
add_column :event_engine_outbox_events, :payload, :jsonb, null: false
|
|
5
|
+
else
|
|
6
|
+
add_column :event_engine_outbox_events, :payload, :json, null: false
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class AddEventVersionToOutboxEvents < ActiveRecord::Migration[7.1]
|
|
2
|
+
def up
|
|
3
|
+
# 1. Add column allowing NULLs
|
|
4
|
+
add_column :event_engine_outbox_events, :event_version, :integer
|
|
5
|
+
|
|
6
|
+
# 2. Backfill existing rows
|
|
7
|
+
execute <<~SQL
|
|
8
|
+
UPDATE event_engine_outbox_events
|
|
9
|
+
SET event_version = 1
|
|
10
|
+
WHERE event_version IS NULL
|
|
11
|
+
SQL
|
|
12
|
+
|
|
13
|
+
# 3. Enforce NOT NULL constraint
|
|
14
|
+
change_column_null :event_engine_outbox_events, :event_version, false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def down
|
|
18
|
+
remove_column :event_engine_outbox_events, :event_version
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddIndexesToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
add_index :event_engine_outbox_events, :published_at
|
|
4
|
+
add_index :event_engine_outbox_events, :dead_lettered_at
|
|
5
|
+
add_index :event_engine_outbox_events, [:published_at, :dead_lettered_at, :created_at],
|
|
6
|
+
name: "idx_outbox_events_publishable"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddAggregateColumnsToEventEngineOutboxEvents < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :event_engine_outbox_events, :aggregate_type, :string
|
|
4
|
+
add_column :event_engine_outbox_events, :aggregate_id, :string
|
|
5
|
+
add_column :event_engine_outbox_events, :aggregate_version, :integer
|
|
6
|
+
add_index :event_engine_outbox_events, [:aggregate_type, :aggregate_id],
|
|
7
|
+
name: "idx_outbox_events_aggregate"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module EventEngine
|
|
6
|
+
module Cloud
|
|
7
|
+
# HTTP client for the EventEngine Cloud ingestion API.
|
|
8
|
+
# Uses +Net::HTTP+ (stdlib) with a 5-second timeout. All errors are
|
|
9
|
+
# rescued and logged — network failures never propagate to the host app.
|
|
10
|
+
class ApiClient
|
|
11
|
+
# @return [Integer] HTTP timeout in seconds
|
|
12
|
+
TIMEOUT = 5
|
|
13
|
+
|
|
14
|
+
# @param api_key [String] the Cloud API key
|
|
15
|
+
# @param endpoint [String] the Cloud API base URL
|
|
16
|
+
def initialize(api_key:, endpoint:)
|
|
17
|
+
@api_key = api_key
|
|
18
|
+
@endpoint = endpoint
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Posts a batch of event entries to the Cloud API.
|
|
22
|
+
#
|
|
23
|
+
# @param entries [Array<Hash>] serialized event metadata entries
|
|
24
|
+
# @return [Boolean] true on success, false on failure
|
|
25
|
+
def send_batch(entries)
|
|
26
|
+
post("/events", { entries: entries })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Posts a heartbeat with app and schema info to the Cloud API.
|
|
30
|
+
#
|
|
31
|
+
# @param heartbeat [Hash] heartbeat data
|
|
32
|
+
# @return [Boolean] true on success, false on failure
|
|
33
|
+
def send_heartbeat(heartbeat)
|
|
34
|
+
post("/heartbeat", heartbeat)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def post(path, body)
|
|
40
|
+
uri = URI("#{@endpoint}#{path}")
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = uri.scheme == "https"
|
|
43
|
+
http.open_timeout = TIMEOUT
|
|
44
|
+
http.read_timeout = TIMEOUT
|
|
45
|
+
|
|
46
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
47
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
48
|
+
request["Content-Type"] = "application/json"
|
|
49
|
+
request["X-EventEngine-Gem-Version"] = EventEngine::VERSION
|
|
50
|
+
request.body = JSON.generate(body)
|
|
51
|
+
|
|
52
|
+
response = http.request(request)
|
|
53
|
+
response.code.start_with?("2")
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
EventEngine::Delivery.configuration.logger.error(
|
|
56
|
+
"[EventEngine::Cloud] API request failed: #{e.class} - #{e.message}"
|
|
57
|
+
)
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Cloud
|
|
3
|
+
# Thread-safe accumulator for Cloud Reporter entries.
|
|
4
|
+
# Entries are pushed individually and drained in bulk for flushing.
|
|
5
|
+
class Batch
|
|
6
|
+
# @param max_size [Integer] triggers auto-flush when reached
|
|
7
|
+
def initialize(max_size:)
|
|
8
|
+
@max_size = max_size
|
|
9
|
+
@entries = []
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Adds an entry to the batch.
|
|
14
|
+
#
|
|
15
|
+
# @param entry [Hash] serialized event metadata
|
|
16
|
+
# @return [Integer] current batch size after push
|
|
17
|
+
def push(entry)
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
@entries << entry
|
|
20
|
+
@entries.size
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Removes and returns all entries, emptying the batch.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Hash>]
|
|
27
|
+
def drain
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
entries = @entries.dup
|
|
30
|
+
@entries.clear
|
|
31
|
+
entries
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether the batch has reached its max size.
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def full?
|
|
39
|
+
@mutex.synchronize { @entries.size >= @max_size }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the current number of entries.
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer]
|
|
45
|
+
def size
|
|
46
|
+
@mutex.synchronize { @entries.size }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Cloud
|
|
3
|
+
# Singleton that collects event metadata and sends it to EventEngine Cloud.
|
|
4
|
+
# Manages the full lifecycle: start, collect via {Subscribers}, batch, flush, shutdown.
|
|
5
|
+
#
|
|
6
|
+
# Activated automatically at boot when +cloud_api_key+ is configured.
|
|
7
|
+
# All errors are rescued — reporter failures never affect the host application.
|
|
8
|
+
class Reporter
|
|
9
|
+
class << self
|
|
10
|
+
# Returns the singleton instance.
|
|
11
|
+
#
|
|
12
|
+
# @return [Reporter]
|
|
13
|
+
def instance
|
|
14
|
+
@instance ||= new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Resets the singleton, discarding state without flushing.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def reset!
|
|
21
|
+
if @instance
|
|
22
|
+
@instance.instance_variable_set(:@running, false)
|
|
23
|
+
timer = @instance.instance_variable_get(:@timer_thread)
|
|
24
|
+
timer&.kill
|
|
25
|
+
@instance = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@running = false
|
|
32
|
+
@batch = nil
|
|
33
|
+
@client = nil
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@timer_thread = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Initializes the batch and API client, begins accepting entries.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def start
|
|
42
|
+
config = EventEngine::Delivery.configuration
|
|
43
|
+
@batch = Batch.new(max_size: config.cloud_batch_size)
|
|
44
|
+
@client = ApiClient.new(
|
|
45
|
+
api_key: config.cloud_api_key,
|
|
46
|
+
endpoint: config.cloud_endpoint
|
|
47
|
+
)
|
|
48
|
+
@running = true
|
|
49
|
+
@flush_interval = config.cloud_flush_interval
|
|
50
|
+
start_timer
|
|
51
|
+
|
|
52
|
+
logger.info("[EventEngine] Cloud Reporter started — reporting to #{config.cloud_endpoint}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Flushes remaining entries and stops the reporter.
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
def shutdown
|
|
59
|
+
return unless @running
|
|
60
|
+
|
|
61
|
+
@running = false
|
|
62
|
+
stop_timer
|
|
63
|
+
flush
|
|
64
|
+
|
|
65
|
+
logger.info("[EventEngine] Cloud Reporter stopped")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Whether the reporter is currently active.
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def running?
|
|
72
|
+
@running
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Records an emitted event entry.
|
|
76
|
+
# @param entry [Hash] serialized event metadata
|
|
77
|
+
# @return [void]
|
|
78
|
+
def track_emit(entry)
|
|
79
|
+
push(entry)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Records a published event entry.
|
|
83
|
+
# @param entry [Hash] serialized event metadata
|
|
84
|
+
# @return [void]
|
|
85
|
+
def track_publish(entry)
|
|
86
|
+
push(entry)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Records a dead-lettered event entry.
|
|
90
|
+
# @param entry [Hash] serialized event metadata
|
|
91
|
+
# @return [void]
|
|
92
|
+
def track_dead_letter(entry)
|
|
93
|
+
push(entry)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Drains the batch and sends entries to the Cloud API.
|
|
97
|
+
# Does nothing if the batch is empty. Errors are logged, never raised.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
def flush
|
|
101
|
+
return unless @batch
|
|
102
|
+
|
|
103
|
+
entries = @batch.drain
|
|
104
|
+
return if entries.empty?
|
|
105
|
+
|
|
106
|
+
@client.send_batch(entries)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
EventEngine::Delivery.configuration.logger.error(
|
|
109
|
+
"[EventEngine::Cloud] Flush failed: #{e.class} - #{e.message}"
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the number of entries currently queued.
|
|
114
|
+
#
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
def batch_size
|
|
117
|
+
@batch&.size || 0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def start_timer
|
|
123
|
+
@timer_thread = Thread.new do
|
|
124
|
+
while @running
|
|
125
|
+
sleep(@flush_interval)
|
|
126
|
+
begin
|
|
127
|
+
flush if @running
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
logger.error("[EventEngine::Cloud] Timer thread error: #{e.class} - #{e.message}")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def stop_timer
|
|
136
|
+
return unless @timer_thread
|
|
137
|
+
|
|
138
|
+
@timer_thread.join([@flush_interval * 2, 5].min)
|
|
139
|
+
@timer_thread.kill if @timer_thread.alive?
|
|
140
|
+
@timer_thread = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def push(entry)
|
|
144
|
+
return unless @running && @batch
|
|
145
|
+
|
|
146
|
+
@batch.push(entry)
|
|
147
|
+
flush if @batch.full?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def logger
|
|
151
|
+
EventEngine::Delivery.configuration.logger
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Cloud
|
|
3
|
+
# Converts +ActiveSupport::Notifications+ payloads into metadata-only
|
|
4
|
+
# entries for the Cloud API. Event payloads and PII are never included.
|
|
5
|
+
class Serializer
|
|
6
|
+
# Serializes an event emission notification.
|
|
7
|
+
#
|
|
8
|
+
# @param notification_payload [Hash] the AS::Notifications payload
|
|
9
|
+
# @return [Hash] metadata entry with +:status+ set to +"emitted"+
|
|
10
|
+
def self.serialize_emit(notification_payload)
|
|
11
|
+
{
|
|
12
|
+
event_id: notification_payload[:event_id],
|
|
13
|
+
event_name: notification_payload[:event_name],
|
|
14
|
+
event_version: notification_payload[:event_version],
|
|
15
|
+
idempotency_key: notification_payload[:idempotency_key],
|
|
16
|
+
aggregate_type: notification_payload[:aggregate_type],
|
|
17
|
+
aggregate_id: notification_payload[:aggregate_id],
|
|
18
|
+
aggregate_version: notification_payload[:aggregate_version],
|
|
19
|
+
status: "emitted",
|
|
20
|
+
timestamp: Time.current.iso8601
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Serializes a publish notification.
|
|
25
|
+
#
|
|
26
|
+
# @param notification_payload [Hash] the AS::Notifications payload
|
|
27
|
+
# @return [Hash] metadata entry with +:status+ set to +"published"+
|
|
28
|
+
def self.serialize_publish(notification_payload)
|
|
29
|
+
{
|
|
30
|
+
event_id: notification_payload[:event_id],
|
|
31
|
+
event_name: notification_payload[:event_name],
|
|
32
|
+
event_version: notification_payload[:event_version],
|
|
33
|
+
status: "published",
|
|
34
|
+
timestamp: Time.current.iso8601
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Serializes a dead-letter notification. Includes error details.
|
|
39
|
+
#
|
|
40
|
+
# @param notification_payload [Hash] the AS::Notifications payload
|
|
41
|
+
# @return [Hash] metadata entry with +:status+ set to +"dead_lettered"+
|
|
42
|
+
def self.serialize_dead_letter(notification_payload)
|
|
43
|
+
{
|
|
44
|
+
event_id: notification_payload[:event_id],
|
|
45
|
+
event_name: notification_payload[:event_name],
|
|
46
|
+
event_version: notification_payload[:event_version],
|
|
47
|
+
status: "dead_lettered",
|
|
48
|
+
attempts: notification_payload[:attempts],
|
|
49
|
+
error_message: notification_payload[:error_message],
|
|
50
|
+
error_class: notification_payload[:error_class],
|
|
51
|
+
timestamp: Time.current.iso8601
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Cloud
|
|
3
|
+
# Subscribes to +ActiveSupport::Notifications+ and feeds serialized
|
|
4
|
+
# event metadata to the {Reporter}.
|
|
5
|
+
class Subscribers
|
|
6
|
+
class << self
|
|
7
|
+
# Subscribes to all EventEngine notifications.
|
|
8
|
+
#
|
|
9
|
+
# @param reporter [Reporter] the reporter to send entries to
|
|
10
|
+
# @return [void]
|
|
11
|
+
def subscribe!(reporter:)
|
|
12
|
+
@subscriptions = []
|
|
13
|
+
@reporter = reporter
|
|
14
|
+
|
|
15
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_emitted") do |*, payload|
|
|
16
|
+
entry = Serializer.serialize_emit(payload)
|
|
17
|
+
@reporter.track_emit(entry)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_published") do |*, payload|
|
|
21
|
+
entry = Serializer.serialize_publish(payload)
|
|
22
|
+
@reporter.track_publish(entry)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("event_engine.event_dead_lettered") do |*, payload|
|
|
26
|
+
entry = Serializer.serialize_dead_letter(payload)
|
|
27
|
+
@reporter.track_dead_letter(entry)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Removes all notification subscriptions.
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
def unsubscribe!
|
|
35
|
+
return unless @subscriptions
|
|
36
|
+
|
|
37
|
+
@subscriptions.each do |subscription|
|
|
38
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
39
|
+
end
|
|
40
|
+
@subscriptions = nil
|
|
41
|
+
@reporter = nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|