messaging 0.0.2 → 3.4.1

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