staged_event 0.0.1

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 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: []