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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +68 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +15 -0
  7. data/Gemfile.lock +222 -0
  8. data/README.md +53 -0
  9. data/Rakefile +5 -30
  10. data/_config.yml +1 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +8 -0
  13. data/exe/messaging +11 -0
  14. data/lib/messaging.rb +62 -11
  15. data/lib/messaging/adapters.rb +35 -0
  16. data/lib/messaging/adapters/kafka.rb +55 -0
  17. data/lib/messaging/adapters/kafka/consumer.rb +101 -0
  18. data/lib/messaging/adapters/kafka/producer.rb +52 -0
  19. data/lib/messaging/adapters/postgres.rb +23 -0
  20. data/lib/messaging/adapters/postgres/advisory_transaction_lock.rb +28 -0
  21. data/lib/messaging/adapters/postgres/serialized_message.rb +74 -0
  22. data/lib/messaging/adapters/postgres/store.rb +66 -0
  23. data/lib/messaging/adapters/postgres/stream.rb +37 -0
  24. data/lib/messaging/adapters/postgres/streams.rb +27 -0
  25. data/lib/messaging/adapters/test.rb +59 -0
  26. data/lib/messaging/adapters/test/consumer.rb +46 -0
  27. data/lib/messaging/adapters/test/store.rb +67 -0
  28. data/lib/messaging/adapters/test/stream.rb +21 -0
  29. data/lib/messaging/base_handler.rb +45 -0
  30. data/lib/messaging/cli.rb +62 -0
  31. data/lib/messaging/config.rb +66 -0
  32. data/lib/messaging/consumer_supervisor.rb +63 -0
  33. data/lib/messaging/exception_handler.rb +15 -0
  34. data/lib/messaging/expected_version.rb +31 -0
  35. data/lib/messaging/instrumentation.rb +21 -0
  36. data/lib/messaging/message.rb +103 -0
  37. data/lib/messaging/message/from_json.rb +31 -0
  38. data/lib/messaging/meter.rb +113 -0
  39. data/lib/messaging/middleware.rb +14 -0
  40. data/lib/messaging/middleware/after_active_record_transaction.rb +13 -0
  41. data/lib/messaging/middleware/rails_wrapper.rb +22 -0
  42. data/lib/messaging/publish.rb +33 -0
  43. data/lib/messaging/rails/railtie.rb +36 -0
  44. data/lib/messaging/resque_worker.rb +11 -0
  45. data/lib/messaging/routes.rb +50 -0
  46. data/lib/messaging/routing.rb +67 -0
  47. data/lib/messaging/routing/background_job_subscriber.rb +11 -0
  48. data/lib/messaging/routing/enqueue_message_handler.rb +15 -0
  49. data/lib/messaging/routing/message_matcher.rb +62 -0
  50. data/lib/messaging/routing/subscriber.rb +35 -0
  51. data/lib/messaging/sidekiq_worker.rb +12 -0
  52. data/lib/messaging/version.rb +1 -1
  53. data/messaging.gemspec +40 -0
  54. metadata +299 -102
  55. data/MIT-LICENSE +0 -20
  56. data/app/models/message.rb +0 -5
  57. data/app/models/receipt.rb +0 -13
  58. data/app/uploaders/attachment_uploader.rb +0 -9
  59. data/db/migrate/20130801214110_initial_migration.rb +0 -22
  60. data/lib/generators/messaging/install_generator.rb +0 -26
  61. data/lib/generators/messaging/templates/initializer.rb +0 -11
  62. data/lib/messaging/concerns/configurable_mailer.rb +0 -13
  63. data/lib/messaging/engine.rb +0 -9
  64. data/lib/messaging/models/messageable.rb +0 -24
  65. data/lib/tasks/messaging_tasks.rake +0 -4
  66. data/test/dummy/README.rdoc +0 -28
  67. data/test/dummy/Rakefile +0 -6
  68. data/test/dummy/app/assets/javascripts/application.js +0 -13
  69. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  70. data/test/dummy/app/controllers/application_controller.rb +0 -5
  71. data/test/dummy/app/helpers/application_helper.rb +0 -2
  72. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  73. data/test/dummy/bin/bundle +0 -3
  74. data/test/dummy/bin/rails +0 -4
  75. data/test/dummy/bin/rake +0 -4
  76. data/test/dummy/config.ru +0 -4
  77. data/test/dummy/config/application.rb +0 -23
  78. data/test/dummy/config/boot.rb +0 -5
  79. data/test/dummy/config/database.yml +0 -25
  80. data/test/dummy/config/environment.rb +0 -5
  81. data/test/dummy/config/environments/development.rb +0 -29
  82. data/test/dummy/config/environments/production.rb +0 -80
  83. data/test/dummy/config/environments/test.rb +0 -36
  84. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  85. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  86. data/test/dummy/config/initializers/inflections.rb +0 -16
  87. data/test/dummy/config/initializers/mime_types.rb +0 -5
  88. data/test/dummy/config/initializers/secret_token.rb +0 -12
  89. data/test/dummy/config/initializers/session_store.rb +0 -3
  90. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  91. data/test/dummy/config/locales/en.yml +0 -23
  92. data/test/dummy/config/routes.rb +0 -56
  93. data/test/dummy/public/404.html +0 -58
  94. data/test/dummy/public/422.html +0 -58
  95. data/test/dummy/public/500.html +0 -57
  96. data/test/dummy/public/favicon.ico +0 -0
  97. data/test/messaging_test.rb +0 -7
  98. data/test/test_helper.rb +0 -15
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
- module Models
3
- autoload :Messageable, 'messaging/models/messageable'
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
- mattr_accessor :user_class
7
- mattr_accessor :attachment_storage
8
- mattr_accessor :allowed_attachment_filetypes
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
- require 'messaging/engine'
11
- require 'messaging/concerns/configurable_mailer'
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