messaging 0.0.2 → 3.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|