messaging 0.0.2 → 3.4.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 +4 -4
- data/.circleci/config.yml +68 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +222 -0
- data/README.md +53 -0
- data/Rakefile +5 -30
- data/_config.yml +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/messaging +11 -0
- data/lib/messaging.rb +62 -11
- data/lib/messaging/adapters.rb +35 -0
- data/lib/messaging/adapters/kafka.rb +55 -0
- data/lib/messaging/adapters/kafka/consumer.rb +101 -0
- data/lib/messaging/adapters/kafka/producer.rb +52 -0
- data/lib/messaging/adapters/postgres.rb +23 -0
- data/lib/messaging/adapters/postgres/advisory_transaction_lock.rb +28 -0
- data/lib/messaging/adapters/postgres/serialized_message.rb +74 -0
- data/lib/messaging/adapters/postgres/store.rb +66 -0
- data/lib/messaging/adapters/postgres/stream.rb +37 -0
- data/lib/messaging/adapters/postgres/streams.rb +27 -0
- data/lib/messaging/adapters/test.rb +59 -0
- data/lib/messaging/adapters/test/consumer.rb +46 -0
- data/lib/messaging/adapters/test/store.rb +67 -0
- data/lib/messaging/adapters/test/stream.rb +21 -0
- data/lib/messaging/base_handler.rb +45 -0
- data/lib/messaging/cli.rb +62 -0
- data/lib/messaging/config.rb +66 -0
- data/lib/messaging/consumer_supervisor.rb +63 -0
- data/lib/messaging/exception_handler.rb +15 -0
- data/lib/messaging/expected_version.rb +31 -0
- data/lib/messaging/instrumentation.rb +21 -0
- data/lib/messaging/message.rb +103 -0
- data/lib/messaging/message/from_json.rb +31 -0
- data/lib/messaging/meter.rb +113 -0
- data/lib/messaging/middleware.rb +14 -0
- data/lib/messaging/middleware/after_active_record_transaction.rb +13 -0
- data/lib/messaging/middleware/rails_wrapper.rb +22 -0
- data/lib/messaging/publish.rb +33 -0
- data/lib/messaging/rails/railtie.rb +36 -0
- data/lib/messaging/resque_worker.rb +11 -0
- data/lib/messaging/routes.rb +50 -0
- data/lib/messaging/routing.rb +67 -0
- data/lib/messaging/routing/background_job_subscriber.rb +11 -0
- data/lib/messaging/routing/enqueue_message_handler.rb +15 -0
- data/lib/messaging/routing/message_matcher.rb +62 -0
- data/lib/messaging/routing/subscriber.rb +35 -0
- data/lib/messaging/sidekiq_worker.rb +12 -0
- data/lib/messaging/version.rb +1 -1
- data/messaging.gemspec +40 -0
- metadata +299 -102
- data/MIT-LICENSE +0 -20
- data/app/models/message.rb +0 -5
- data/app/models/receipt.rb +0 -13
- data/app/uploaders/attachment_uploader.rb +0 -9
- data/db/migrate/20130801214110_initial_migration.rb +0 -22
- data/lib/generators/messaging/install_generator.rb +0 -26
- data/lib/generators/messaging/templates/initializer.rb +0 -11
- data/lib/messaging/concerns/configurable_mailer.rb +0 -13
- data/lib/messaging/engine.rb +0 -9
- data/lib/messaging/models/messageable.rb +0 -24
- data/lib/tasks/messaging_tasks.rake +0 -4
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -13
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -23
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -25
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -29
- data/test/dummy/config/environments/production.rb +0 -80
- data/test/dummy/config/environments/test.rb +0 -36
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -12
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/public/404.html +0 -58
- data/test/dummy/public/422.html +0 -58
- data/test/dummy/public/500.html +0 -57
- data/test/dummy/public/favicon.ico +0 -0
- data/test/messaging_test.rb +0 -7
- data/test/test_helper.rb +0 -15
data/bin/setup
ADDED
data/exe/messaging
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
STDOUT.sync = true
|
3
|
+
|
4
|
+
# Load Rails environment if it exists
|
5
|
+
env_path = File.expand_path('config/environment.rb')
|
6
|
+
require env_path if File.exist? env_path
|
7
|
+
|
8
|
+
require_relative '../lib/messaging/cli'
|
9
|
+
|
10
|
+
cli = Messaging::CLI.new
|
11
|
+
cli.run
|
data/lib/messaging.rb
CHANGED
@@ -1,17 +1,68 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'dry-configurable'
|
3
|
+
require 'virtus'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/notifications'
|
6
|
+
require 'active_support/core_ext'
|
7
|
+
|
8
|
+
require 'messaging/config'
|
9
|
+
require 'messaging/expected_version'
|
10
|
+
require 'messaging/instrumentation'
|
11
|
+
require 'messaging/meter'
|
12
|
+
require 'messaging/exception_handler'
|
13
|
+
require 'messaging/message'
|
14
|
+
require 'messaging/message/from_json'
|
15
|
+
require 'messaging/routing'
|
16
|
+
require 'messaging/routes'
|
17
|
+
require 'messaging/base_handler'
|
18
|
+
require 'messaging/adapters'
|
19
|
+
require 'messaging/middleware'
|
20
|
+
require 'messaging/publish'
|
21
|
+
require 'messaging/rails/railtie' if defined? ::Rails
|
22
|
+
|
1
23
|
module Messaging
|
2
|
-
|
3
|
-
|
24
|
+
def self.setup(&block)
|
25
|
+
Config.setup(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.config
|
29
|
+
Config.config
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
Config.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.consumer_adapter
|
37
|
+
Adapters[Config.consumer.adapter]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.dispatcher
|
41
|
+
Adapters::Dispatcher.default
|
4
42
|
end
|
5
43
|
|
6
|
-
|
7
|
-
|
8
|
-
|
44
|
+
def self.message_store
|
45
|
+
Adapters::Store.default
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.[](adapter)
|
49
|
+
Adapters[adapter]
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.routes
|
53
|
+
@routes ||= Routes.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.stream(name)
|
57
|
+
name = name.stream_name if name.respond_to?(:stream_name)
|
58
|
+
message_store.stream(name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.defined_messages
|
62
|
+
ObjectSpace.each_object(Class).select { |c| c.included_modules.include? Messaging::Message }
|
63
|
+
end
|
9
64
|
|
10
|
-
|
11
|
-
|
12
|
-
class << self
|
13
|
-
def setup
|
14
|
-
yield self
|
15
|
-
end
|
65
|
+
def self.in_consumer_mode?
|
66
|
+
File.basename($PROGRAM_NAME) == 'messaging'
|
16
67
|
end
|
17
68
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'dry/container'
|
2
|
+
|
3
|
+
module Messaging
|
4
|
+
module Adapters
|
5
|
+
extend Dry::Container::Mixin
|
6
|
+
|
7
|
+
class Consumer
|
8
|
+
extend Dry::Container::Mixin
|
9
|
+
|
10
|
+
def self.default
|
11
|
+
resolve(Config.consumer.adapter)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Dispatcher
|
16
|
+
extend Dry::Container::Mixin
|
17
|
+
|
18
|
+
def self.default
|
19
|
+
resolve(Config.dispatcher.adapter)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Store
|
24
|
+
extend Dry::Container::Mixin
|
25
|
+
|
26
|
+
def self.default
|
27
|
+
resolve(Config.message_store.adapter)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'messaging/adapters/kafka'
|
34
|
+
require 'messaging/adapters/postgres'
|
35
|
+
require 'messaging/adapters/test'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'kafka'
|
2
|
+
|
3
|
+
require_relative 'kafka/consumer'
|
4
|
+
require_relative 'kafka/producer'
|
5
|
+
|
6
|
+
module Messaging
|
7
|
+
module Adapters
|
8
|
+
# Internal: Adapter for producing and consuming messages with Kafka
|
9
|
+
class Kafka
|
10
|
+
# Internal: Ruby-Kafka client for the current thread.
|
11
|
+
#
|
12
|
+
# We keep one per thread as the client itself is not meant to be shared.
|
13
|
+
# See https://github.com/zendesk/ruby-kafka#thread-safety for more information.
|
14
|
+
def client
|
15
|
+
Thread.current[:messaging_kafka_client] ||= create_kafka_client
|
16
|
+
end
|
17
|
+
|
18
|
+
# The producer doesn't need to be per thread as it
|
19
|
+
# uses a background thread to deliver messages.
|
20
|
+
def dispatcher
|
21
|
+
@dispatcher ||= Producer.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_consumer(name, **options)
|
25
|
+
Consumer.new(name: name, kafka_adapter: self, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal: Setup a Ruby-Kafka consumer
|
29
|
+
def create_kafka_consumer(group_id)
|
30
|
+
client.consumer({ group_id: group_id }.merge(Config.kafka.consumer.to_hash))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Internal: Setup a Ruby-Kafka client
|
36
|
+
def create_kafka_client
|
37
|
+
# We have a specific logger for Ruby-Kafka as it
|
38
|
+
# is a bit more noisy than we would like.
|
39
|
+
kafka_logger = Messaging.logger.dup.tap { |logger| logger.level = Config.kafka.log_level }
|
40
|
+
::Kafka.new(Config.kafka.client.to_hash.merge(client_id: Config.app_name, logger: kafka_logger))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.register!
|
44
|
+
return if Adapters.key? :kafka
|
45
|
+
|
46
|
+
Adapters.register(:kafka, memoize: true) { Kafka.new }
|
47
|
+
Adapters::Consumer.register(:kafka, memoize: true) { Adapters[:kafka] }
|
48
|
+
Adapters::Dispatcher.register(:kafka, memoize: true) { Adapters[:kafka].dispatcher }
|
49
|
+
end
|
50
|
+
private_class_method :register!
|
51
|
+
|
52
|
+
register!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'dry-initializer'
|
2
|
+
module Messaging
|
3
|
+
module Adapters
|
4
|
+
class Kafka
|
5
|
+
# Internal: Wraps a Ruby-Kafka consumer
|
6
|
+
#
|
7
|
+
# Subscribes to topics and dispatches messages
|
8
|
+
# to a handler for doing the actual work.
|
9
|
+
class Consumer
|
10
|
+
include Messaging::Instrumentation
|
11
|
+
include Messaging::Routing
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
option :kafka_adapter
|
15
|
+
option :name
|
16
|
+
|
17
|
+
option :group_id, default: -> { Config.app_name + '-' + name.to_s.underscore }
|
18
|
+
option :logger, default: -> { Messaging.logger }
|
19
|
+
option :consumer, default: -> { kafka_adapter.create_kafka_consumer(group_id) }
|
20
|
+
|
21
|
+
def start
|
22
|
+
logger.info "Consumer #{name} started"
|
23
|
+
subscribe_kafka_consumer_to_topics
|
24
|
+
@running = true
|
25
|
+
process_messages
|
26
|
+
ensure
|
27
|
+
stop if @running
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
logger.info "Consumer #{name} stopping"
|
32
|
+
@running = false
|
33
|
+
consumer&.stop
|
34
|
+
end
|
35
|
+
|
36
|
+
def log_current_status
|
37
|
+
logger.info "Current offsets for #{name}:"
|
38
|
+
logger.info '|- no messages yet' if current_offsets.empty?
|
39
|
+
current_offsets.each { |partition, offset| logger.info "|- #{partition}: #{offset}" }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def subscribe_kafka_consumer_to_topics
|
45
|
+
topics.each { |topic| consumer.subscribe(topic, start_from_beginning: false) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_messages
|
49
|
+
consumer.each_message do |message|
|
50
|
+
process_message(message)
|
51
|
+
current_offsets["#{message.topic}#{message.partition}"] = message.offset
|
52
|
+
end
|
53
|
+
rescue ::Kafka::ProcessingError => e
|
54
|
+
report_processing_exception(e)
|
55
|
+
pause topic: e.topic, partition: e.partition
|
56
|
+
|
57
|
+
retry if @running
|
58
|
+
end
|
59
|
+
|
60
|
+
def current_offsets
|
61
|
+
@current_offsets ||= {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def report_processing_exception(e)
|
65
|
+
logger.error "Error processing partition #{e.topic}/#{e.partition} at offset #{e.offset}"
|
66
|
+
ExceptionHandler.call(
|
67
|
+
e.cause,
|
68
|
+
kafka: {
|
69
|
+
consumer: name,
|
70
|
+
topic: e.topic,
|
71
|
+
partition: e.partition,
|
72
|
+
offset: e.offset
|
73
|
+
}
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def pause(topic:, partition:, timeout: Config.kafka.pause_timeout)
|
78
|
+
return unless timeout.positive?
|
79
|
+
|
80
|
+
logger.warn "Pausing partition #{topic}/#{partition} for #{timeout} s"
|
81
|
+
consumer.pause(topic, partition, timeout: timeout)
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_message(raw_message)
|
85
|
+
metadata = {
|
86
|
+
group_id: group_id,
|
87
|
+
key: raw_message.key,
|
88
|
+
topic: raw_message.topic,
|
89
|
+
partition: raw_message.partition,
|
90
|
+
offset: raw_message.offset
|
91
|
+
}
|
92
|
+
|
93
|
+
message = Message::FromJson.(raw_message.value)
|
94
|
+
instrument('consumer.process_message', metadata) do
|
95
|
+
Middleware.run(Config.consumer.middlewares, message) { handle message }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Messaging
|
2
|
+
module Adapters
|
3
|
+
class Kafka
|
4
|
+
class Producer
|
5
|
+
attr_reader :producer
|
6
|
+
attr_reader :pid
|
7
|
+
attr_reader :kafka
|
8
|
+
|
9
|
+
def initialize(kafka_adapter)
|
10
|
+
@kafka = kafka_adapter
|
11
|
+
connect
|
12
|
+
|
13
|
+
at_exit { shutdown }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Delivers a message to Kafka asynchronously in a background thread.
|
17
|
+
# This method will return immediately.
|
18
|
+
#
|
19
|
+
# @param message
|
20
|
+
def call(message)
|
21
|
+
reconnect if forked?
|
22
|
+
producer.produce(message.to_json, key: message.message_key, topic: message.topic)
|
23
|
+
rescue ::Kafka::BufferOverflow => exception
|
24
|
+
Trouble.notify(exception, pid: Process.pid, message: message.to_json)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
return unless producer
|
29
|
+
|
30
|
+
producer.deliver_messages
|
31
|
+
producer.shutdown
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect
|
35
|
+
@producer = create_producer
|
36
|
+
@pid = Process.pid
|
37
|
+
end
|
38
|
+
alias reconnect connect
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def forked?
|
43
|
+
Process.pid != pid
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_producer
|
47
|
+
kafka.client.async_producer(Config.kafka.producer.to_hash)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require_relative 'postgres/serialized_message'
|
3
|
+
require_relative 'postgres/advisory_transaction_lock'
|
4
|
+
|
5
|
+
require_relative 'postgres/store'
|
6
|
+
module Messaging
|
7
|
+
module Adapters
|
8
|
+
# Adapter for using Postgres and Active Record as a message store.
|
9
|
+
# @see Messaging::Adapters::Postgres::Store Store - for more information on how to use the message store
|
10
|
+
# capabilities provided by this adapter.
|
11
|
+
class Postgres
|
12
|
+
def self.register!
|
13
|
+
return if Adapters.key? :postgres
|
14
|
+
|
15
|
+
Adapters.register(:postgres, memoize: true) { Postgres.new }
|
16
|
+
Adapters::Store.register(:postgres, memoize: true) { Store.new }
|
17
|
+
end
|
18
|
+
private_class_method :register!
|
19
|
+
|
20
|
+
register!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'method_object'
|
2
|
+
|
3
|
+
module Messaging
|
4
|
+
module Adapters
|
5
|
+
class Postgres
|
6
|
+
# Creates an advisory lock that is held during the rest of the transaction.
|
7
|
+
# If another session is trying to aquire the same lock it will wait until the
|
8
|
+
# lock is released.
|
9
|
+
# @api private
|
10
|
+
class AdvisoryTransactionLock
|
11
|
+
include MethodObject
|
12
|
+
|
13
|
+
option :key
|
14
|
+
option :connection, default: -> { SerializedMessage.connection }
|
15
|
+
|
16
|
+
def call
|
17
|
+
connection.execute "SELECT pg_advisory_xact_lock(#{lock_key});"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def lock_key
|
23
|
+
Zlib.crc32(key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Messaging
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
class SerializedMessage < ActiveRecord::Base
|
5
|
+
self.table_name = :messaging_messages
|
6
|
+
|
7
|
+
attr_accessor :expected_version
|
8
|
+
|
9
|
+
# We override this AR method to make records retreived
|
10
|
+
# from scopes etc. be message objects of the corresponding
|
11
|
+
# message class instead of AR objects
|
12
|
+
#
|
13
|
+
# See https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-instantiate
|
14
|
+
def self.instantiate(attributes, *_args)
|
15
|
+
attributes['message_type'].constantize.new(
|
16
|
+
JSON.parse(attributes['data']).merge(stream_position: attributes['stream_position'])
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Virtual setter for message so we can create a serialized message from a message
|
21
|
+
def message=(message)
|
22
|
+
self.data = message.attributes_as_json
|
23
|
+
self.expected_version = message.expected_version
|
24
|
+
self.message_type = message.message_type
|
25
|
+
self.stream = message.stream_name
|
26
|
+
self.uuid = message.uuid
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_message
|
30
|
+
message_type.constantize.new(data.merge(stream_position: stream_position))
|
31
|
+
end
|
32
|
+
|
33
|
+
# You should never update a message after creating it
|
34
|
+
def readonly?
|
35
|
+
true unless new_record?
|
36
|
+
end
|
37
|
+
|
38
|
+
def stream
|
39
|
+
@stream ||= Stream.new(attributes['stream'])
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_or_update(*args)
|
43
|
+
with_locked_stream do
|
44
|
+
set_stream_position
|
45
|
+
validate_expected_version!
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Memoize the current version to not make multiple SQL queries on create
|
53
|
+
def current_stream_position
|
54
|
+
@current_stream_position ||= stream.current_position
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_stream_position
|
58
|
+
self.stream_position = current_stream_position + 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_expected_version!
|
62
|
+
ExpectedVersion.new(expected_version || :any).match!(current_stream_position)
|
63
|
+
end
|
64
|
+
|
65
|
+
def with_locked_stream
|
66
|
+
transaction do
|
67
|
+
AdvisoryTransactionLock.call key: stream.name
|
68
|
+
yield
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|