staged_event 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 626ddab89e43654e2f9f7444efbbb778e40a02882142e5bab971a253d0586278
4
+ data.tar.gz: 77bf67900459f69099d58913d8ae49140c934d49239f1ebf043e0924d127632e
5
+ SHA512:
6
+ metadata.gz: 19f8edb5ba417a59bae2d5e62f68437d34a1563b3996fe2d848c6515292136970f071ef7ed75bbc6e9bdac7404a317b5330ba8d1cd3a4bfca40ca0160ea7ba1b
7
+ data.tar.gz: 7dc404d84752a7aeeda6897cbb47dda3168d86219e3fdc809e21b5dac230a829d2b266fb361f51ac21d0acf0b5ab3bd566a5121181b4853644b4b272a1f55f4e
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # StagedEvent
2
+
3
+ StagedEvent is a Ruby implementation of the [transactional outbox](https://microservices.io/patterns/data/transactional-outbox.html) architectural pattern.
4
+
5
+ This gem is relevant for any process that wants to publish events to a set of subscribers. It automatically saves outgoing events as records in a database table, from which a separate background process reads, publishes, and (once publishing is confirmed) deletes them. StagedEvent also provides a listener to subscribe to and receive these events from other processes.
6
+
7
+ Staging events this way allows them to be persisted within the same database transaction as associated domain-specific records, guaranteeing at-least-once delivery and thus eventual consistency with the receiving system(s).
8
+
9
+ For now, StagedEvent makes the following assumptions:
10
+ - Events are defined as protobufs (and you have generated Ruby classes for them)
11
+ - [Google Pub/Sub](https://cloud.google.com/pubsub/) is the underyling messaging infrastructure
12
+ - You're using ActiveRecord (with a Postgres database >= version 9.5, due to the usage of the SQL `SKIP LOCKED` clause).
13
+
14
+ ### Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "staged_event"
20
+ ```
21
+
22
+ Then, in your application directory:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ $ rails generate staged_event:install
27
+ ```
28
+
29
+ This will create an initializer at `config/initializers/staged_event.rb`, which you should modify with settings specific to your application.
30
+
31
+ The installer will also generate a migration to create the database table for staged events waiting to be published. Remember to run it using:
32
+
33
+ ```bash
34
+ rails db:migrate
35
+ ```
36
+
37
+ ### Publishing Events
38
+
39
+ In order for events to actually be published, StagedEvent provides a rake task that should be kept running alongside your application server(s):
40
+
41
+ ```bash
42
+ rake staged_event:publisher
43
+ ```
44
+
45
+ ### Receiving Events
46
+
47
+ In order to receive events from publishers, StagedEvent provides a rake task that should be kept running alongside your application server(s):
48
+
49
+ ```bash
50
+ rake staged_event:subscriber
51
+ ```
52
+
53
+ In order to process incoming events, you define a callback in the staged_event initializer file.
54
+
55
+ ### Regenerating Ruby from protobufs
56
+
57
+ StagedEvent uses a one-off protobuf definition to serialize and deserialize events that are defined as protobufs. In case it becomes necessary to recreate the auto-generated ruby, the command for that (from the repository root) is:
58
+
59
+ protoc --ruby_out=./ "./lib/staged_event/event_envelope.proto"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module StagedEvent
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ class << self
11
+ delegate :next_migration_number, to: ActiveRecord::Generators::Base
12
+ end
13
+
14
+ source_paths << File.join(File.dirname(__FILE__), "templates")
15
+
16
+ def create_migration_file
17
+ migration_template "create_staged_events.rb.erb", "db/migrate/create_staged_events.rb"
18
+ end
19
+
20
+ def create_initializer
21
+ template "initializer.rb.erb", "config/initializers/staged_event.rb"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ class CreateStagedEvents < ActiveRecord::Migration[6.0]
2
+ def change
3
+ enable_extension "uuid-ossp"
4
+ enable_extension "pgcrypto"
5
+
6
+ create_table :staged_events, id: :uuid do |t|
7
+ t.string :topic
8
+ t.binary :data, null: false
9
+ t.datetime :created_at, null: false
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ StagedEvent::Configuration.configure do |config|
4
+ config.google_pubsub do |google_pubsub|
5
+ # The contents of the Pub/Sub keyfile as a Hash
6
+ google_pubsub.credentials = {}
7
+
8
+ # This specifies the mapping between the "topic" values stored on events in
9
+ # the database, and the associated Pub/Sub topic id. This level of indirection
10
+ # lets you use arbitrary topic names in your application code instead of being
11
+ # tied to names stored in a third-party infrastructure.
12
+ # The first entry in the map is used as the default for when the event does
13
+ # not specify a topic.
14
+ google_pubsub.topic_map = {
15
+ default: "topic_id_1",
16
+ }
17
+
18
+ google_pubsub.subscription_ids = [ "subscription_id_1" ]
19
+ end
20
+
21
+ config.publisher do |publisher|
22
+ # The number of events that a publisher will transactionally publish and remove
23
+ # from the database in each iteration.
24
+ publisher.batch_size = 5
25
+ end
26
+
27
+ config.subscriber do |subscriber|
28
+ # This callback will be run for every message received, and is passed the serialized
29
+ # event data.
30
+ subscriber.event_received_callback = lambda do |serialized_data|
31
+ #
32
+ # To respond to the the event, one could either
33
+ # 1) enqueue a background job with serialized_data as a parameter
34
+ # 2) handle the event in-process. For example:
35
+ #
36
+ # event = StagedEvent.deserialize_event(serialized_data)
37
+ #
38
+ # event.id is globally-unique and can be used for idempotency.
39
+ # event.data is the event deserialized into its original form (typically a
40
+ # protobuf object).
41
+ #
42
+ # if event.data.is_a?(MyEvents::SomethingHappened)
43
+ # handle_something_happening
44
+ # end
45
+ #
46
+ # WARNING: At the moment, the google pub/sub subscriber only has one thread
47
+ # per subscription id, so execution of this callback _blocks_ receipt of
48
+ # subsequent events for the subscription.
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ class BackoffTimer
5
+ INCREMENTS_BEFORE_BACKOFF = 5
6
+ MAX_VALUE = 25
7
+
8
+ def initialize
9
+ reset
10
+ end
11
+
12
+ def increment
13
+ @increments += 1
14
+ end
15
+
16
+ def reset
17
+ @increments = 0
18
+ end
19
+
20
+ def value
21
+ # returns 1 until the timer has been incremented n times, after which it
22
+ # returns n^2 (until reaching a set maximum)
23
+ backoff_time = [ 1, increments - INCREMENTS_BEFORE_BACKOFF + 1 ].max**2
24
+ [ backoff_time, MAX_VALUE ].min
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :increments
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "directive"
4
+
5
+ module StagedEvent
6
+ module Configuration
7
+ extend Directive
8
+
9
+ configuration_options do
10
+ nested :google_pubsub do
11
+ option :credentials
12
+ option :topic_map
13
+ option :subscription_ids
14
+ end
15
+
16
+ nested :publisher do
17
+ option :batch_size
18
+ end
19
+
20
+ nested :subscriber do
21
+ option :event_received_callback
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ syntax = 'proto3';
2
+
3
+ package staged_event;
4
+
5
+ import "google/protobuf/any.proto";
6
+
7
+ // EventEnvelope allows any protobuf object to be embedded into its Any field.
8
+ // By doing so, type information about the embedded object is included. This allows
9
+ // a receiver to deserialize the object without knowing its type ahead of time.
10
+
11
+ message EventEnvelope {
12
+ google.protobuf.Any event = 1;
13
+ string uuid = 2;
14
+ }
@@ -0,0 +1,18 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # source: lib/staged_event/event_envelope.proto
3
+
4
+ require 'google/protobuf'
5
+
6
+ require 'google/protobuf/any_pb'
7
+ Google::Protobuf::DescriptorPool.generated_pool.build do
8
+ add_file("lib/staged_event/event_envelope.proto", :syntax => :proto3) do
9
+ add_message "staged_event.EventEnvelope" do
10
+ optional :event, :message, 1, "google.protobuf.Any"
11
+ optional :uuid, :string, 2
12
+ end
13
+ end
14
+ end
15
+
16
+ module StagedEvent
17
+ EventEnvelope = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("staged_event.EventEnvelope").msgclass
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ module GooglePubSub
5
+ class Helper
6
+ class << self
7
+ def new_google_pubsub
8
+ credentials = Configuration.config.google_pubsub.credentials.to_h
9
+ project_id = credentials[:project_id]
10
+
11
+ Google::Cloud::PubSub.new(
12
+ project_id: project_id,
13
+ credentials: credentials,
14
+ )
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ module GooglePubSub
5
+ class Publisher < StagedEvent::Publisher
6
+ def initialize
7
+ @google_pubsub = Helper.new_google_pubsub
8
+
9
+ raise ArgumentError, "topic_map must be initialized" unless topic_map.is_a?(Hash) && topic_map.any?
10
+ end
11
+
12
+ def publish(model)
13
+ topic_name = model.topic || topic_map.keys.first
14
+ topic_id = topic_map.fetch(topic_name)
15
+ google_topic = google_pubsub.topic(topic_id, skip_lookup: true)
16
+
17
+ # https://github.com/googleapis/google-cloud-ruby/blob/720d14d3641a60c0fab0bf8519bdd730a753a897/google-cloud-pubsub/lib/google/cloud/pubsub/topic.rb#L655
18
+ google_topic.publish(model.data)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :google_pubsub
24
+
25
+ def topic_map
26
+ Configuration.config.google_pubsub.topic_map
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ module GooglePubSub
5
+ class Subscriber < StagedEvent::Subscriber
6
+ include Technologic
7
+
8
+ def initialize
9
+ @google_pubsub = Helper.new_google_pubsub
10
+
11
+ raise ArgumentError, "event_received_callback is undefined" unless event_received_callback.respond_to?(:call)
12
+ end
13
+
14
+ def receive_events
15
+ threads = []
16
+
17
+ subscription_ids.each do |subscription_id|
18
+ threads << Thread.new do
19
+ receive_events_from_subscription(subscription_id)
20
+ end
21
+ end
22
+
23
+ threads.each(&:join)
24
+ end
25
+
26
+ def receive_events_from_subscription(subscription_id)
27
+ subscription = google_pubsub.subscription(subscription_id)
28
+
29
+ loop do
30
+ received_messages = subscription.pull(immediate: false)
31
+ received_messages.each do |received_message|
32
+ event_received_callback.call(received_message.data)
33
+ received_message.acknowledge!
34
+ end
35
+ end
36
+ rescue StandardError => exception
37
+ error :subscription_failed, exception: exception.message
38
+ retry
39
+ end
40
+
41
+ # TODO: Google pub/sub has built-in multi-threaded listeners but I haven't
42
+ # been able to successfully receive any messages using that API yet.
43
+ #
44
+ # def receive_events_from_subscription(subscription_id)
45
+ # subscription = google_pubsub.subscription(subscription_id)
46
+ # subscriber = subscription.listen(threads: { callback: 5 }) do |message|
47
+ # event_received_callback.call(message.data)
48
+ # message.acknowledge!
49
+ # end
50
+ #
51
+ # # Start background threads that will call the block passed to listen.
52
+ # subscriber.start
53
+ #
54
+ # # Block, letting processing threads continue in the background
55
+ # sleep
56
+ # end
57
+
58
+ private
59
+
60
+ attr_reader :google_pubsub
61
+
62
+ def subscription_ids
63
+ Configuration.config.google_pubsub.subscription_ids
64
+ end
65
+
66
+ def event_received_callback
67
+ Configuration.config.subscriber.event_received_callback
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module StagedEvent
6
+ class Model < ActiveRecord::Base
7
+ self.table_name = :staged_events
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ class Publisher
5
+ def publish(_model)
6
+ raise StandardError, "You must implement the publish method"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ class PublisherProcess
5
+ include Technologic
6
+
7
+ def initialize(publisher)
8
+ @publisher = publisher
9
+ @backoff_timer = BackoffTimer.new
10
+ end
11
+
12
+ def run
13
+ loop do
14
+ sleep(backoff_timer.value)
15
+ publish_next_batch
16
+ end
17
+ rescue StandardError => exception
18
+ error :publish_failed, exception: exception.message
19
+ backoff_timer.increment
20
+ retry
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :publisher, :backoff_timer
26
+
27
+ def publish_next_batch
28
+ ActiveRecord::Base.connection_pool.with_connection do
29
+ ActiveRecord::Base.transaction do
30
+ events = Model.order(:created_at).limit(batch_size).lock("FOR UPDATE SKIP LOCKED")
31
+ if events.any?
32
+ events.each do |event|
33
+ publisher.publish(event)
34
+ end
35
+ events.destroy_all
36
+ backoff_timer.reset
37
+ else
38
+ backoff_timer.increment
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def batch_size
45
+ Configuration.config.publisher.batch_size
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Labyrinth
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load "tasks/publisher.rake"
9
+ load "tasks/subscriber.rake"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ class Subscriber
5
+ def receive_events
6
+ raise StandardError, "You must implement the receive_events method"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ class SubscriberProcess
5
+ include Technologic
6
+
7
+ def initialize(subscriber)
8
+ @subscriber = subscriber
9
+ end
10
+
11
+ def run
12
+ subscriber.receive_events
13
+ rescue StandardError => exception
14
+ error :subscriber_main_loop_failed, exception: exception.message
15
+ retry
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :subscriber
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StagedEvent
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "google/cloud/pubsub"
4
+
5
+ require_relative "staged_event/backoff_timer"
6
+ require_relative "staged_event/configuration"
7
+ require_relative "staged_event/event_envelope_pb"
8
+ require_relative "staged_event/model"
9
+ require_relative "staged_event/publisher"
10
+ require_relative "staged_event/subscriber"
11
+ require_relative "staged_event/publisher_process"
12
+ require_relative "staged_event/subscriber_process"
13
+ require_relative "staged_event/version"
14
+
15
+ require_relative "staged_event/google_pub_sub/helper"
16
+ require_relative "staged_event/google_pub_sub/publisher"
17
+ require_relative "staged_event/google_pub_sub/subscriber"
18
+
19
+ require_relative "staged_event/railtie" if defined?(Rails)
20
+
21
+ module StagedEvent
22
+ class Error < StandardError; end
23
+
24
+ class DeserializationError < Error; end
25
+ class UnknownEventTypeError < Error; end
26
+
27
+ class << self
28
+ # Builds an ActiveRecord model from a proto object representing an event.
29
+ # If the model is committed to the database, it will be published by the
30
+ # publisher process.
31
+ #
32
+ # @param [Instance of a protobuf generated class] proto the event data to publish
33
+ # @option kwargs [String] :topic the topic that will receive the event
34
+ # @return [StagedEvent::Model] an ActiveRecord model ready to be saved
35
+ def from_proto(proto, **kwargs)
36
+ uuid = SecureRandom.uuid
37
+
38
+ envelope = EventEnvelope.new(
39
+ event: {
40
+ type_url: proto.class.descriptor.name,
41
+ value: proto.class.encode(proto),
42
+ },
43
+ uuid: uuid,
44
+ )
45
+
46
+ data = EventEnvelope.encode(envelope)
47
+ topic = kwargs.fetch(:topic, nil)
48
+
49
+ Model.new(id: uuid, data: data, topic: topic)
50
+ end
51
+
52
+ # Converts serialized event data received from a publisher into an object with
53
+ # the event (and its metadata) in accessible form
54
+ #
55
+ # @param [String] serialized_data the serialized event data
56
+ # @return [OpenStruct] an object representing the event
57
+ def deserialize_event(serialized_data)
58
+ envelope = EventEnvelope.decode(serialized_data)
59
+ event_descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(envelope.event.type_url)
60
+ raise UnknownEventTypeError if event_descriptor.blank?
61
+
62
+ proto = event_descriptor.msgclass.decode(envelope.event.value)
63
+
64
+ OpenStruct.new(
65
+ id: envelope.uuid,
66
+ data: proto,
67
+ )
68
+ rescue Google::Protobuf::ParseError
69
+ raise DeserializationError
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,6 @@
1
+ namespace :staged_event do
2
+ desc "Run the staged_event publisher process"
3
+ task :publisher => :environment do
4
+ StagedEvent::PublisherProcess.new(StagedEvent::GooglePubSub::Publisher.new).run
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ namespace :staged_event do
2
+ desc "Run the staged_event subscriber process"
3
+ task :subscriber => :environment do |task, args|
4
+ StagedEvent::SubscriberProcess.new(StagedEvent::GooglePubSub::Subscriber.new).run
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,209 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: staged_event
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Cross
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: directive
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.23.1.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.23.1.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: google-protobuf
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.8.0
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 4.0.0
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.8.0
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 4.0.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: google-cloud-pubsub
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: byebug
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: database_cleaner
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: faker
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rspec
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: sqlite3
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: rake
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ description: A transactional outbox implementation for Ruby/ActiveRecord
160
+ email: andrew.cross@freshly.com
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files: []
164
+ files:
165
+ - README.md
166
+ - lib/generators/staged_event/install_generator.rb
167
+ - lib/generators/staged_event/templates/create_staged_events.rb.erb
168
+ - lib/generators/staged_event/templates/initializer.rb.erb
169
+ - lib/staged_event.rb
170
+ - lib/staged_event/backoff_timer.rb
171
+ - lib/staged_event/configuration.rb
172
+ - lib/staged_event/event_envelope.proto
173
+ - lib/staged_event/event_envelope_pb.rb
174
+ - lib/staged_event/google_pub_sub/helper.rb
175
+ - lib/staged_event/google_pub_sub/publisher.rb
176
+ - lib/staged_event/google_pub_sub/subscriber.rb
177
+ - lib/staged_event/model.rb
178
+ - lib/staged_event/publisher.rb
179
+ - lib/staged_event/publisher_process.rb
180
+ - lib/staged_event/railtie.rb
181
+ - lib/staged_event/subscriber.rb
182
+ - lib/staged_event/subscriber_process.rb
183
+ - lib/staged_event/version.rb
184
+ - lib/tasks/publisher.rake
185
+ - lib/tasks/subscriber.rake
186
+ homepage: https://github.com/Freshly/staged_event
187
+ licenses:
188
+ - MIT
189
+ metadata: {}
190
+ post_install_message:
191
+ rdoc_options: []
192
+ require_paths:
193
+ - lib
194
+ required_ruby_version: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - ">="
197
+ - !ruby/object:Gem::Version
198
+ version: '0'
199
+ required_rubygems_version: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ requirements: []
205
+ rubygems_version: 3.2.15
206
+ signing_key:
207
+ specification_version: 4
208
+ summary: StagedEvent
209
+ test_files: []