From fd2428fe508806793c17f17598139ab375526727 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 14:42:10 -0400 Subject: [PATCH 01/16] iniital stubbing of external_store feature --- Gemfile.lock | 12 ++++----- lib/mantle.rb | 3 +++ lib/mantle/configuration.rb | 3 ++- lib/mantle/external_store/active_record.rb | 15 +++++++++++ lib/mantle/external_store/redis.rb | 15 +++++++++++ lib/mantle/external_store_manager.rb | 22 ++++++++++++++++ lib/mantle/message.rb | 7 ++++- lib/mantle/version.rb | 2 +- .../lib/mantle/external_store_manager_spec.rb | 15 +++++++++++ spec/lib/mantle/message_spec.rb | 26 +++++++++++++++++++ 10 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 lib/mantle/external_store/active_record.rb create mode 100644 lib/mantle/external_store/redis.rb create mode 100644 lib/mantle/external_store_manager.rb create mode 100644 spec/lib/mantle/external_store_manager_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 24beb64..b0cea6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mantle (2.3.0) + mantle (2.4.0) redis sidekiq (~> 5.0) @@ -9,14 +9,14 @@ GEM remote: https://rubygems.org/ specs: coderay (1.1.0) - connection_pool (2.2.2) + connection_pool (2.2.3) diff-lcs (1.2.5) method_source (0.8.2) pry (0.10.0) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - rack (2.0.9) + rack (2.2.3) rack-protection (2.0.8.1) rack redis (4.1.3) @@ -33,11 +33,11 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-support (3.2.2) - sidekiq (5.2.8) + sidekiq (5.2.9) connection_pool (~> 2.2, >= 2.2.2) - rack (< 2.1.0) + rack (~> 2.0) rack-protection (>= 1.5.0) - redis (>= 3.3.5, < 5) + redis (>= 3.3.5, < 4.2) slop (3.5.0) PLATFORMS diff --git a/lib/mantle.rb b/lib/mantle.rb index 9221a90..7812927 100644 --- a/lib/mantle.rb +++ b/lib/mantle.rb @@ -11,6 +11,9 @@ require_relative 'mantle/catch_up' require_relative 'mantle/configuration' require_relative 'mantle/error' +require_relative 'mantle/external_store_manager' +require_relative 'mantle/external_store/redis' +require_relative 'mantle/external_store/active_record' require_relative 'mantle/local_redis' require_relative 'mantle/logger' require_relative 'mantle/message' diff --git a/lib/mantle/configuration.rb b/lib/mantle/configuration.rb index 7cfa6c6..dd01563 100644 --- a/lib/mantle/configuration.rb +++ b/lib/mantle/configuration.rb @@ -3,7 +3,8 @@ class Configuration attr_accessor :message_bus_redis, :logger, :redis_namespace, - :whoami + :whoami, + :external_store attr_reader :message_handlers diff --git a/lib/mantle/external_store/active_record.rb b/lib/mantle/external_store/active_record.rb new file mode 100644 index 0000000..e83dab6 --- /dev/null +++ b/lib/mantle/external_store/active_record.rb @@ -0,0 +1,15 @@ +module Mantle + module ExternalStore + class ActiveRecord + def store(external_payload) + # TODO: implement actual store for active_record + { external_store: :active_record, + uuid: 'uuid' } + end + + def retriev(uuid) + # TODO: implement actual retrieve for active_record + end + end + end +end diff --git a/lib/mantle/external_store/redis.rb b/lib/mantle/external_store/redis.rb new file mode 100644 index 0000000..0db2e67 --- /dev/null +++ b/lib/mantle/external_store/redis.rb @@ -0,0 +1,15 @@ +module Mantle + module ExternalStore + class Redis + def store(external_payload) + # TODO: implement actual store for redis + { external_store: :redis, + uuid: 'uuid' } + end + + def retriev(uuid) + # TODO: implement actual retrieve for redis + end + end + end +end diff --git a/lib/mantle/external_store_manager.rb b/lib/mantle/external_store_manager.rb new file mode 100644 index 0000000..84fb282 --- /dev/null +++ b/lib/mantle/external_store_manager.rb @@ -0,0 +1,22 @@ +module Mantle + class ExternalStoreManager + def self.store(external_store:, external_payload:) + classify(external_store).new.store(external_payload) + end + + def self.retriev(external_store:, uuid:_) + classify(external_store).new.retrieve(uuid) + end + + def self.classify(external_store) + builtin_stores[external_store.to_sym] || external_store + end + + def self.builtin_stores + @builtin_stores ||= { + redis: Mantle::ExternalStore::Redis, + active_record: Mantle::ExternalStore::ActiveRecord + } + end + end +end diff --git a/lib/mantle/message.rb b/lib/mantle/message.rb index 3e3cda5..779fd55 100644 --- a/lib/mantle/message.rb +++ b/lib/mantle/message.rb @@ -9,8 +9,9 @@ def initialize(channel) @catch_up = Mantle::CatchUp.new end - def publish(message) + def publish(message, external_payload = nil) message = message.merge(__MANTLE__: { message_source: whoami }) if whoami + message = message.merge(external_payload: store(external_payload)) if external_payload message_bus.publish(channel, message) catch_up.add_message(channel, message) end @@ -22,5 +23,9 @@ def publish(message) def whoami Mantle.configuration.whoami end + + def store(external_payload) + Mantle::ExternalStoreManager.store(external_store: Mantle.configuration.external_store, external_payload: external_payload) + end end end diff --git a/lib/mantle/version.rb b/lib/mantle/version.rb index 43bc2d5..4b36391 100644 --- a/lib/mantle/version.rb +++ b/lib/mantle/version.rb @@ -1,3 +1,3 @@ module Mantle - VERSION = '2.3.0' + VERSION = '2.4.0' end diff --git a/spec/lib/mantle/external_store_manager_spec.rb b/spec/lib/mantle/external_store_manager_spec.rb new file mode 100644 index 0000000..3f6d0a1 --- /dev/null +++ b/spec/lib/mantle/external_store_manager_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Mantle::ExternalStoreManager do + describe "#store" do + it "stores the external_payload to the specified store" do + # TODO: Finish spec + end + end + + describe "#retrieve" do + it "trieves the external_payload from the specified store" do + # TODO: Finish spec + end + end +end diff --git a/spec/lib/mantle/message_spec.rb b/spec/lib/mantle/message_spec.rb index 8f71cfc..4ad6d29 100644 --- a/spec/lib/mantle/message_spec.rb +++ b/spec/lib/mantle/message_spec.rb @@ -41,5 +41,31 @@ expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message) end + + it "allows external payload store" do + Mantle.configure do |config| + config.whoami = 'SantaClaus' + config.external_store = :redis # or :active_record? + end + bus = double("message bus") + catch_up = double("catch up") + channel = "create:person" + message = { id: 1 } + actual_message = message.merge(__MANTLE__: { message_source: 'SantaClaus' }, external_payload: { external_store: :redis, uuid: 'uuid' }) + + external_payload = { some: :really, huge: [ { payload: "containing", misc: "stuff" } ] } + + mantle_message = Mantle::Message.new(channel) + mantle_message.message_bus = bus + mantle_message.catch_up = catch_up + + allow(bus).to receive(:publish) + allow(catch_up).to receive(:add_message) + + mantle_message.publish(message, external_payload) + + expect(bus).to have_received(:publish).with(channel, actual_message) + expect(catch_up).to have_received(:add_message).with(channel, actual_message) + end end end From 247cdb58e63f8d4317cd1f31d6fa80206a5bc18f Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 14:46:35 -0400 Subject: [PATCH 02/16] add circleci config --- .circleci/config.yml | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4d58449 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,74 @@ +defaults: &defaults + working_directory: ~/mantle + parallelism: 1 + docker: + - image: circleci/ruby:2.6.2 + environment: + RAILS_ENV: test + PGHOST: 127.0.0.1 + PGUSER: postgres + - image: circleci/postgres:9.3-alpine + environment: + POSTGRES_USER: postgres + - image: redis + +version: 2 +jobs: + build: + <<: *defaults + steps: + - run: sudo apt-get update && sudo apt-get install -y r-base postgresql-client || true + + - restore_cache: + key: v1-mantle-repo-{{ .Environment.CIRCLE_SHA1 }} + + - checkout + + - save_cache: + key: v1-mantle-repo-{{ .Environment.CIRCLE_SHA1 }} + paths: + - ~/mantle + + - restore_cache: + key: v1-mantle-bundle-{{ checksum "Gemfile.lock" }} + + - run: + name: install bundler + command: | + gem install bundler:2.0.2 + + - run: + name: bundle install + command: | + bundle install --jobs=4 --retry=3 --path vendor/bundle + + - save_cache: + paths: + - ~/mantle/vendor/bundle + key: v1-mantle-bundle-{{ checksum "Gemfile.lock" }} + + - run: + name: Wait for DB + command: dockerize -wait tcp://localhost:5432 -timeout 1m + + - run: bundle exec rake db:create + - run: bundle exec rake db:structure:load + - run: bundle exec rake db:seed + + - run: + name: Rspec + command: | + mkdir /tmp/test-results + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" + + bundle exec rspec --format progress \ + --format RspecJunitFormatter \ + --out test_results/rspec.xml \ + -- $TEST_FILES + + - store_test_results: + path: test_results + + - store_artifacts: + path: test-results/rspec.xml + destination: test-results From e8b1ad42c16179b2ab2edb1af713be636575e77c Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 14:59:42 -0400 Subject: [PATCH 03/16] attempt to update circle config --- .circleci/config.yml | 5 ----- circle.yml | 3 --- 2 files changed, 8 deletions(-) delete mode 100644 circle.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d58449..828b347 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,11 +32,6 @@ jobs: - restore_cache: key: v1-mantle-bundle-{{ checksum "Gemfile.lock" }} - - run: - name: install bundler - command: | - gem install bundler:2.0.2 - - run: name: bundle install command: | diff --git a/circle.yml b/circle.yml deleted file mode 100644 index d28dd4b..0000000 --- a/circle.yml +++ /dev/null @@ -1,3 +0,0 @@ -machine: - ruby: - version: '2.2' From 38c1c93d52c9c733cfb762ea1a1858f42b94f075 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 15:55:31 -0400 Subject: [PATCH 04/16] update bundler --- .circleci/config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 828b347..6b443f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,6 +32,11 @@ jobs: - restore_cache: key: v1-mantle-bundle-{{ checksum "Gemfile.lock" }} + - run: + name: install bundler + command: | + gem install bundler:2.1.4 + - run: name: bundle install command: | From ce17a4e390b85061484b366839c2c6579affc723 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 15:57:21 -0400 Subject: [PATCH 05/16] no need for db yet --- .circleci/config.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b443f0..4925bf3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,10 +51,6 @@ jobs: name: Wait for DB command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: bundle exec rake db:create - - run: bundle exec rake db:structure:load - - run: bundle exec rake db:seed - - run: name: Rspec command: | From 9729cf2436e5344486434c337e81697ea7f731a8 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 25 Mar 2022 16:00:13 -0400 Subject: [PATCH 06/16] remove formatter --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4925bf3..71c5f61 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,7 +58,6 @@ jobs: TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" bundle exec rspec --format progress \ - --format RspecJunitFormatter \ --out test_results/rspec.xml \ -- $TEST_FILES From 5b7733fb51247f1b2adfe3d3177b33dd91509041 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 08:56:39 -0400 Subject: [PATCH 07/16] add ability to send some of the payload via an external store (redis) --- Gemfile.lock | 4 +++- lib/mantle.rb | 1 + lib/mantle/configuration.rb | 12 ++++++++--- lib/mantle/external_store/active_record.rb | 4 ++++ lib/mantle/external_store/redis.rb | 16 +++++++++++++-- lib/mantle/external_store_manager.rb | 23 ++++++++++++++-------- lib/mantle/message.rb | 4 ++-- mantle.gemspec | 1 + spec/lib/mantle/message_spec.rb | 6 +++--- 9 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b0cea6e..1b80417 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH mantle (2.4.0) redis sidekiq (~> 5.0) + uuidtools GEM remote: https://rubygems.org/ @@ -39,6 +40,7 @@ GEM rack-protection (>= 1.5.0) redis (>= 3.3.5, < 4.2) slop (3.5.0) + uuidtools (2.2.0) PLATFORMS ruby @@ -49,4 +51,4 @@ DEPENDENCIES rspec BUNDLED WITH - 2.1.4 + 2.3.5 diff --git a/lib/mantle.rb b/lib/mantle.rb index 7812927..9ea3a26 100644 --- a/lib/mantle.rb +++ b/lib/mantle.rb @@ -2,6 +2,7 @@ require 'redis' require 'sidekiq' require 'json' +require 'uuidtools' begin require 'pry' diff --git a/lib/mantle/configuration.rb b/lib/mantle/configuration.rb index dd01563..6d0a02b 100644 --- a/lib/mantle/configuration.rb +++ b/lib/mantle/configuration.rb @@ -3,10 +3,10 @@ class Configuration attr_accessor :message_bus_redis, :logger, :redis_namespace, - :whoami, - :external_store + :whoami - attr_reader :message_handlers + attr_reader :message_handlers, + :external_store_manager def initialize @message_handlers = Mantle::MessageHandlers.new @@ -17,5 +17,11 @@ def initialize def message_handlers=(hash_instance) @message_handlers = Mantle::MessageHandlers.new(hash_instance) end + + def external_store=(args) + external_store, options = args + @external_store_manager ||= Mantle::ExternalStoreManager.new + @external_store_manager.configure(external_store, options || {}) + end end end diff --git a/lib/mantle/external_store/active_record.rb b/lib/mantle/external_store/active_record.rb index e83dab6..a206c0d 100644 --- a/lib/mantle/external_store/active_record.rb +++ b/lib/mantle/external_store/active_record.rb @@ -1,6 +1,10 @@ module Mantle module ExternalStore class ActiveRecord + def configure(options) + @table = options[:table] + end + def store(external_payload) # TODO: implement actual store for active_record { external_store: :active_record, diff --git a/lib/mantle/external_store/redis.rb b/lib/mantle/external_store/redis.rb index 0db2e67..d7b6e4b 100644 --- a/lib/mantle/external_store/redis.rb +++ b/lib/mantle/external_store/redis.rb @@ -1,14 +1,26 @@ module Mantle module ExternalStore class Redis + def configure(options) + @redis = options[:redis] + end + def store(external_payload) - # TODO: implement actual store for redis + uuid = new_uuid + @redis.set(uuid, external_payload) { external_store: :redis, - uuid: 'uuid' } + uuid: uuid } end def retriev(uuid) # TODO: implement actual retrieve for redis + @redis.get(uuid) + end + + private + + def new_uuid + UUIDTools::UUID.timestamp_create.to_s end end end diff --git a/lib/mantle/external_store_manager.rb b/lib/mantle/external_store_manager.rb index 84fb282..daa04e4 100644 --- a/lib/mantle/external_store_manager.rb +++ b/lib/mantle/external_store_manager.rb @@ -1,19 +1,26 @@ module Mantle class ExternalStoreManager - def self.store(external_store:, external_payload:) - classify(external_store).new.store(external_payload) + attr_accessor :external_stores + + def configure(external_store, options) + instance(external_store).configure(options) + end + + def store(external_store:, external_payload:) + instance(external_store).store(external_payload) end - def self.retriev(external_store:, uuid:_) - classify(external_store).new.retrieve(uuid) + def retriev(external_store:, uuid:_) + instance(external_store).new.retrieve(uuid) end - def self.classify(external_store) - builtin_stores[external_store.to_sym] || external_store + def instance(external_store) + @external_stores ||= {} + @external_stores[external_store] ||= (builtin_stores[external_store.to_sym] || external_store).new end - def self.builtin_stores - @builtin_stores ||= { + def builtin_stores + @@builtin_stores ||= { redis: Mantle::ExternalStore::Redis, active_record: Mantle::ExternalStore::ActiveRecord } diff --git a/lib/mantle/message.rb b/lib/mantle/message.rb index 779fd55..78989c4 100644 --- a/lib/mantle/message.rb +++ b/lib/mantle/message.rb @@ -24,8 +24,8 @@ def whoami Mantle.configuration.whoami end - def store(external_payload) - Mantle::ExternalStoreManager.store(external_store: Mantle.configuration.external_store, external_payload: external_payload) + def store(external_store: external_store, external_payload: external_payload) + Mantle.configuration.external_store_manager.store(external_store: external_store, external_payload: external_payload) end end end diff --git a/mantle.gemspec b/mantle.gemspec index 4bff6f1..6e5ac5d 100644 --- a/mantle.gemspec +++ b/mantle.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |gem| gem.add_dependency('redis') gem.add_dependency('sidekiq', '~> 5.0') + gem.add_dependency('uuidtools') gem.add_development_dependency('rspec') gem.add_development_dependency('pry') diff --git a/spec/lib/mantle/message_spec.rb b/spec/lib/mantle/message_spec.rb index 4ad6d29..71776a4 100644 --- a/spec/lib/mantle/message_spec.rb +++ b/spec/lib/mantle/message_spec.rb @@ -45,13 +45,13 @@ it "allows external payload store" do Mantle.configure do |config| config.whoami = 'SantaClaus' - config.external_store = :redis # or :active_record? + config.external_store = [ :redis, redis: Redis.new ] # or :active_record? end bus = double("message bus") catch_up = double("catch up") channel = "create:person" message = { id: 1 } - actual_message = message.merge(__MANTLE__: { message_source: 'SantaClaus' }, external_payload: { external_store: :redis, uuid: 'uuid' }) + actual_message = message.merge(__MANTLE__: { message_source: 'SantaClaus' }, external_payload: { external_store: :redis, uuid: instance_of(String) }) external_payload = { some: :really, huge: [ { payload: "containing", misc: "stuff" } ] } @@ -62,7 +62,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message, external_payload) + mantle_message.publish(message, external_payload: external_payload, external_store: :redis) expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message) From b7638d6971fa8d28ccac8e9e318f777a884af4e8 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 10:13:08 -0400 Subject: [PATCH 08/16] update README --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index 6724dd9..3635adb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ or install manually by: ## Usage (in Rails App) +### Configure + Setup a Rails initializer(`config/initializers/mantle.rb`): @@ -60,6 +62,7 @@ Mantle.configure do |config| end ``` +### Publish Messages (Publisher) Publish messages to consumers: @@ -72,6 +75,8 @@ message on. The `#publish` method takes the message payload (in any format you l and pushes the message on to the message bus pub/sub and also adds it to the catch up queue so offline applications can process the message when they become available. +### Receive Messages (Listeners) + Define message handler class with `.receive` method. For example `app/models/my_message_handler.rb` ```Ruby @@ -111,6 +116,60 @@ $ bin/sidekiq -q mantle -q default It will NOT add the `default` queue to processing if there are other queues enumerated using the `-q` option. +### Large Payloads + +Because Mantle uses Redis for the message bus, sending very large messages can quickly use a lot of Redis store, +and in the event that Redis memory is exceeded, your app will be rendered inoperable. In addition, a Mantle handle may +need to pass the `message` on to other services (for example, queue processors), passing very large messages can +compound in even greater memory usage. + +For this reason, it sometimes makes sense to send large payloads through an external key/value store where the handler can +pull in the payload. + +#### Configuring External Store + +Mantle facilitates sending large payloads with limited impact on your message bus memory usage by allowing you to +configure an external store by adding the following in the initializer. + +``` Ruby + config.external_store = { redis: Redis.new(host: 'localhost') } # default: no external store +``` + +#### Publishing with External Payloads + +Consumers expecting external payloads will receive a third argument on the `receive` method: + +```Ruby +Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { body: 'large_external_payload' } ) +``` + +#### Retrieving External Payloads + +The consumers will then receive an additional to retrieve that `external_payload` (using the same `external_store` config), by calling: + +```Ruby +class MyMessageHandler + def self.receive(channel, message, uuid) + puts channel # => 'order' + puts message # => { 'id' => 5, 'name' => 'Brandon' } + puts external_store_uuid # => '' + puts Mantle.retrieve_external_payload(uuid) # => { 'body' => 'large_external_payload' } + end +end +``` + +If a consumer is only expecting to receive 2 arguments, then Mantle will detect this (using Ruby reflection), and it +will retrieve the payload and merge it into the `message`, passing this on to the `receive` method as part of `message`. + +```Ruby +class MyMessageHandler + def self.receive(channel, message) + puts channel # => 'order' + puts message # => { 'id' => 5, 'name' => 'Brandon', 'body' => 'large_external_payload' } + end +end +``` + ## Testing Requiring this library causes messages to be appended to an in-memory array. From cf496f78f5fb69783083c0e15d2aba0e29aaecd5 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 10:16:39 -0400 Subject: [PATCH 09/16] tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3635adb..7dc65f4 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ enumerated using the `-q` option. Because Mantle uses Redis for the message bus, sending very large messages can quickly use a lot of Redis store, and in the event that Redis memory is exceeded, your app will be rendered inoperable. In addition, a Mantle handle may need to pass the `message` on to other services (for example, queue processors), passing very large messages can -compound in even greater memory usage. +compound to even greater memory usage for the same exact payload. For this reason, it sometimes makes sense to send large payloads through an external key/value store where the handler can pull in the payload. From 1c0e8c91589ab5e32bca2d2b8ad056b2c8ed9312 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 10:17:30 -0400 Subject: [PATCH 10/16] tweak again --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7dc65f4..eeab929 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ enumerated using the `-q` option. Because Mantle uses Redis for the message bus, sending very large messages can quickly use a lot of Redis store, and in the event that Redis memory is exceeded, your app will be rendered inoperable. In addition, a Mantle handle may need to pass the `message` on to other services (for example, queue processors), passing very large messages can -compound to even greater memory usage for the same exact payload. +compound resulting in even greater memory usage for the same exact payload. For this reason, it sometimes makes sense to send large payloads through an external key/value store where the handler can pull in the payload. From 1a117f077005d476e93b4427f697ffbc72a12c53 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 10:21:51 -0400 Subject: [PATCH 11/16] clarify docs --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eeab929..0195773 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ message on. The `#publish` method takes the message payload (in any format you l and pushes the message on to the message bus pub/sub and also adds it to the catch up queue so offline applications can process the message when they become available. -### Receive Messages (Listeners) +### Receive Messages (Consumer) Define message handler class with `.receive` method. For example `app/models/my_message_handler.rb` @@ -132,7 +132,11 @@ Mantle facilitates sending large payloads with limited impact on your message bu configure an external store by adding the following in the initializer. ``` Ruby +Mantle.configure do |config| + ... config.external_store = { redis: Redis.new(host: 'localhost') } # default: no external store + ... +end ``` #### Publishing with External Payloads From adc92654e8a29847824bb9ff9a342d91fbd7daf5 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 10:48:23 -0400 Subject: [PATCH 12/16] add active record to README --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0195773..e69191d 100644 --- a/README.md +++ b/README.md @@ -131,14 +131,46 @@ pull in the payload. Mantle facilitates sending large payloads with limited impact on your message bus memory usage by allowing you to configure an external store by adding the following in the initializer. +External store can use either `redis` (optionally, a different `Redis` instance) or `ActiveRecord`. + +To use `redis` use a hash to configure external store: + ``` Ruby Mantle.configure do |config| ... - config.external_store = { redis: Redis.new(host: 'localhost') } # default: no external store + config.external_store = { redis: Redis.new(host: 'localhost') } ... end ``` +To use `ActiveRecord` use a hash to configure external store: + +``` Ruby +Mantle.configure do |config| + ... + config.external_store = { table_name: `my_external_payloads`, database: {...} } + ... +end +``` + +The `database` hash will be passed to ActiveRecord `establish_connection`. + +The `table_name` specified must be a table in the database, and must contain the following columns: + +```Ruby + create_table :my_external_payloads do |t| + t.string :uuid, nil: false + t.text :payload, nil: false + t.timestamp :keep_until, nil: true + t.timestamp :expire_at, nil: true + t.timestamp :created_at, nil: false + end + + add_index :my_external_payloads, :uuid + add_index :my_external_payloads, :expire_at + add_index :my_external_payloads, :created_at +``` + #### Publishing with External Payloads Consumers expecting external payloads will receive a third argument on the `receive` method: From 750bc054aa7d0aaede376273358cc07f1916d6b6 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Wed, 30 Mar 2022 13:04:02 -0400 Subject: [PATCH 13/16] update docs --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e69191d..44a3d24 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ class MyMessageHandler end ``` +### Listener / Processor + To run the listener: ``` @@ -173,15 +175,26 @@ The `table_name` specified must be a table in the database, and must contain the #### Publishing with External Payloads -Consumers expecting external payloads will receive a third argument on the `receive` method: +An external payload can added to the publish method as using a hash as the third argument. The actual payload is passed as part of the `payload` key. + +```Ruby +Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' } } ) +``` + +There are three ways the `ExternalStoreManager` will free memory. +- If a `keep_until` parameter is specified, then the payload will not be freed until that time. The payload may outlive the specified time, based on `least recently created`. +- If an `expire_at` parameter is specified, then the payload will be freed at that time. The payload will not outlive the specified time. +- If neither qualifier is specified by the publisher, then `least recently created` will be freed, as needed. ```Ruby -Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { body: 'large_external_payload' } ) +Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' }, keep_until: 3.hours.from_now } ) + +Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' }, expire_at: 3.hours.from_now } ) ``` #### Retrieving External Payloads -The consumers will then receive an additional to retrieve that `external_payload` (using the same `external_store` config), by calling: +The consumers expeceting an external payload will then receive an additional to retrieve that `external_payload` (using the same `external_store` config), by calling: ```Ruby class MyMessageHandler From 05185a105686ca275e52f7a1d216e4a1cd2c1e09 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Thu, 31 Mar 2022 10:44:39 -0400 Subject: [PATCH 14/16] update readme, add named arguments --- README.md | 99 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 44a3d24..2f06c39 100644 --- a/README.md +++ b/README.md @@ -67,18 +67,35 @@ end Publish messages to consumers: ```Ruby -Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }) +Mantle::Message.new("person:create").publish(message: { id: message['id'], data: message['data'] }) ``` The first and only argument to `Mantle::Message.new` is the channel you want to publish the -message on. The `#publish` method takes the message payload (in any format you like) -and pushes the message on to the message bus pub/sub and also adds it to the +message on. The `#publish` method takes a named argument `message:` which contains the message content (in any structure you like). +This pushes the `message` on to the message bus pub/sub and also adds it to the catch up queue so offline applications can process the message when they become available. +Note that you can still use a bare argument for the message, but this will be deprecated in the future: + +```Ruby +Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }) +``` + ### Receive Messages (Consumer) Define message handler class with `.receive` method. For example `app/models/my_message_handler.rb` +```Ruby +class MyMessageHandler + def self.receive(channel:, message:) + puts channel # => 'order' + puts message # => { 'id' => 5, 'name' => 'Brandon' } + end +end +``` + +Note that you can still use two bare arguments for the channel and message, but this will be deprecated in the future: + ```Ruby class MyMessageHandler def self.receive(channel, message) @@ -88,6 +105,7 @@ class MyMessageHandler end ``` + ### Listener / Processor To run the listener: @@ -126,7 +144,22 @@ need to pass the `message` on to other services (for example, queue processors), compound resulting in even greater memory usage for the same exact payload. For this reason, it sometimes makes sense to send large payloads through an external key/value store where the handler can -pull in the payload. +retrieve the payload only when needed instead of pass the entire payload as part of the message. + +Note that rather than move the entire payload to external store, it often makes sense for the handler to have a small amount +of data available without retrieving the external payload, such as `account_id` so the handler can do something like this in +the top of the handler (this reveals a named argument `uuid` which is documented below): + +```Ruby +class MyMessageHandler + def self.receive(channel:, message:, uuid:) + return unless interesting_account?(message['account_id']) + + puts channel # => 'order' + puts message # => { 'id' => 5, 'name' => 'Brandon' } + end +end +``` #### Configuring External Store @@ -135,29 +168,29 @@ configure an external store by adding the following in the initializer. External store can use either `redis` (optionally, a different `Redis` instance) or `ActiveRecord`. -To use `redis` use a hash to configure external store: +To use `redis` use a hash to configure an `external_store_manager`: ``` Ruby Mantle.configure do |config| ... - config.external_store = { redis: Redis.new(host: 'localhost') } + config.external_store_manager = { redis: Redis.new(host: 'localhost'), keep_for: 3.hours } # default: keep_for: nil ... end ``` -To use `ActiveRecord` use a hash to configure external store: +To use `ActiveRecord` use a hash to configure an `external_store_manager`: ``` Ruby Mantle.configure do |config| ... - config.external_store = { table_name: `my_external_payloads`, database: {...} } + config.external_store_manager = { table_name: `my_external_payloads`, database: {...}, keep_for: 3.hours } # default: keep_for: nil ... end ``` The `database` hash will be passed to ActiveRecord `establish_connection`. -The `table_name` specified must be a table in the database, and must contain the following columns: +The `table_name` specified must be a table in the database, and must be creating using the following migration: ```Ruby create_table :my_external_payloads do |t| @@ -175,10 +208,12 @@ The `table_name` specified must be a table in the database, and must contain the #### Publishing with External Payloads -An external payload can added to the publish method as using a hash as the third argument. The actual payload is passed as part of the `payload` key. +An external payload can added to the publish method as using a named argument, `payload:`: ```Ruby -Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' } } ) +the_payload = { body: 'large_external_payload' } +the_message = { id: message['id'], data: message['data'] } +Mantle::Message.new("person:create").publish(payload: the_payload, message: the_message) ``` There are three ways the `ExternalStoreManager` will free memory. @@ -187,18 +222,22 @@ There are three ways the `ExternalStoreManager` will free memory. - If neither qualifier is specified by the publisher, then `least recently created` will be freed, as needed. ```Ruby -Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' }, keep_until: 3.hours.from_now } ) +Mantle::Message.new("person:create").publish(message: { id: message['id'], data: message['data'] }, payload: { body: 'large_external_payload' }, keep_until: 3.hours.from_now) -Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] }, { payload: { body: 'large_external_payload' }, expire_at: 3.hours.from_now } ) +Mantle::Message.new("person:create").publish(message: { id: message['id'], data: message['data'] }, payload: { body: 'large_external_payload' }, expire_at: 3.hours.from_now) ``` #### Retrieving External Payloads -The consumers expeceting an external payload will then receive an additional to retrieve that `external_payload` (using the same `external_store` config), by calling: +A handler (consumer) does not need to be aware there is an external payload. If it does not define a named argument `uuid`, +then the Mantle processor will retrieve the payload and merge it into the message before calling the handler. + +If, however, the handler is aware of the extneral payload, then it simply defineds a named argument `uuid` in the method, and +the `uuid` will be set, and the `payload` will not be merged into message. ```Ruby class MyMessageHandler - def self.receive(channel, message, uuid) + def self.receive(channel:, message:, uuid:) puts channel # => 'order' puts message # => { 'id' => 5, 'name' => 'Brandon' } puts external_store_uuid # => '' @@ -207,16 +246,28 @@ class MyMessageHandler end ``` -If a consumer is only expecting to receive 2 arguments, then Mantle will detect this (using Ruby reflection), and it -will retrieve the payload and merge it into the `message`, passing this on to the `receive` method as part of `message`. +One may want to keep the payload separate from the message so that the handler can pass the `uuid` as a parameter to a queue processor. This would avoid +always adding large payloads to Sidekiq parameters (for example). In this case, the sidekiq processor would also need to be aware of the `uuid` and can call -```Ruby -class MyMessageHandler - def self.receive(channel, message) - puts channel # => 'order' - puts message # => { 'id' => 5, 'name' => 'Brandon', 'body' => 'large_external_payload' } - end -end +```Reuby + Mantle.external_store_managers.retrieve(uuid: uuid) +``` + +#### Using External Store Directly + +Using the concept of avoiding sending large payloads to queue processors may make senese even outside of Mantle handlers. + +For this reason, the `external_store_manager` is available to be used outside of Mantle: + +```Reuby + uuid = Mantle.external_store_managers.store(payload: "my large payload") +``` + +and then within the processor: + + +```Reuby + Mantle.external_store_managers.retrieve(uuid: uuid) ``` ## Testing From bdd5cb7375c7f5d27127d51b745732a4cd1e8f69 Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Thu, 31 Mar 2022 17:29:58 -0400 Subject: [PATCH 15/16] first pass, making code match README --- lib/mantle.rb | 4 ++ lib/mantle/external_store/active_record.rb | 3 +- lib/mantle/external_store/redis.rb | 3 +- lib/mantle/external_store_manager.rb | 23 +++++----- lib/mantle/message.rb | 22 +++++++--- lib/mantle/workers/message_handler_worker.rb | 46 ++++++++++++++++++-- spec/lib/mantle/message_spec.rb | 13 +++--- 7 files changed, 84 insertions(+), 30 deletions(-) diff --git a/lib/mantle.rb b/lib/mantle.rb index 9ea3a26..f0287bf 100644 --- a/lib/mantle.rb +++ b/lib/mantle.rb @@ -46,6 +46,10 @@ def self.receive_message(channel, message) self.configuration.message_handlers.receive_message channel, message end + def self.external_store_manager + configuration.external_store_manager + end + def self.channels configuration.message_handlers.channels end diff --git a/lib/mantle/external_store/active_record.rb b/lib/mantle/external_store/active_record.rb index a206c0d..2033b25 100644 --- a/lib/mantle/external_store/active_record.rb +++ b/lib/mantle/external_store/active_record.rb @@ -7,8 +7,7 @@ def configure(options) def store(external_payload) # TODO: implement actual store for active_record - { external_store: :active_record, - uuid: 'uuid' } + 'uuid' end def retriev(uuid) diff --git a/lib/mantle/external_store/redis.rb b/lib/mantle/external_store/redis.rb index d7b6e4b..495daf4 100644 --- a/lib/mantle/external_store/redis.rb +++ b/lib/mantle/external_store/redis.rb @@ -8,8 +8,7 @@ def configure(options) def store(external_payload) uuid = new_uuid @redis.set(uuid, external_payload) - { external_store: :redis, - uuid: uuid } + uuid end def retriev(uuid) diff --git a/lib/mantle/external_store_manager.rb b/lib/mantle/external_store_manager.rb index daa04e4..f9eb6bf 100644 --- a/lib/mantle/external_store_manager.rb +++ b/lib/mantle/external_store_manager.rb @@ -1,22 +1,23 @@ module Mantle class ExternalStoreManager - attr_accessor :external_stores - - def configure(external_store, options) - instance(external_store).configure(options) + def configure(external_store_type, options) + store_for(external_store_type).configure(options) end - def store(external_store:, external_payload:) - instance(external_store).store(external_payload) + def store(payload:, keep_for: nil, expires_in: nil) + external_store.store(payload) end - def retriev(external_store:, uuid:_) - instance(external_store).new.retrieve(uuid) + def retriev(uuid:) + external_store.retrieve(uuid) end - def instance(external_store) - @external_stores ||= {} - @external_stores[external_store] ||= (builtin_stores[external_store.to_sym] || external_store).new + private + + attr_accessor :external_store + + def store_for(external_store_type) + @external_store ||= (builtin_stores[external_store_type] || external_store_type).new end def builtin_stores diff --git a/lib/mantle/message.rb b/lib/mantle/message.rb index 78989c4..480ab87 100644 --- a/lib/mantle/message.rb +++ b/lib/mantle/message.rb @@ -9,23 +9,33 @@ def initialize(channel) @catch_up = Mantle::CatchUp.new end - def publish(message, external_payload = nil) - message = message.merge(__MANTLE__: { message_source: whoami }) if whoami - message = message.merge(external_payload: store(external_payload)) if external_payload + def publish(message: nil, payload: nil, expires_in: nil, keep_for: nil) + # Add __MANTLE__ meta-data... + mantle_meta_data(sent_at: Time.now) + mantle_meta_data(message_source: whoami) if whoami + mantle_meta_data(uuid: store(payload: payload, expires_in: expires_in, keep_for: keep_for)) if payload + message[:__MANTLE__] = meta_data + message_bus.publish(channel, message) catch_up.add_message(channel, message) end private - attr_reader :message_bus, :catch_up + attr_reader :message_bus, :catch_up, :meta_data + + def mantle_meta_data(meta_data) + @meta_data ||= { } + @meta_data.merge!(meta_data) + @meta_data + end def whoami Mantle.configuration.whoami end - def store(external_store: external_store, external_payload: external_payload) - Mantle.configuration.external_store_manager.store(external_store: external_store, external_payload: external_payload) + def store(payload:, expires_in:, keep_for:) + Mantle.configuration.external_store_manager.store(payload: payload, expires_in: expires_in, keep_for: keep_for) end end end diff --git a/lib/mantle/workers/message_handler_worker.rb b/lib/mantle/workers/message_handler_worker.rb index 6d367e2..807837e 100644 --- a/lib/mantle/workers/message_handler_worker.rb +++ b/lib/mantle/workers/message_handler_worker.rb @@ -5,11 +5,51 @@ class MessageHandlerWorker sidekiq_options queue: :mantle + attr_reader :handler, :channel, :message, :uuid + def perform(string_handler, channel, message) - handler = Object.const_get(string_handler) - handler.receive channel, message + @handler = Object.const_get(string_handler) + @channel = channel + @message = message + + # Use reflection to decide what to do here... + notify_handler + end + + def notify_handler + merge_payload_if_needed + + case + when uses_named_arguments? && expects_uuid? + handler.receive(channel: channel, message: message, uuid: uuid) + when uses_named_arguments? && !expects_uuid? + handler.receive(channel: channel, message: message) + when !uses_named_arguments? && expects_uuid? + handler.receive(channel, message, uuid: uuid) + when !uses_named_arguments? && !expects_uuid? + handler.receive(channel, message) + end + end + + def merge_payload_if_needed + if uuid && !expects_uuid? + payload = Mantle.external_store_manager.retrieve(uuid) + # This will work if both are hashes... + message.merge!(payload) + end + end + + def expects_uuid? + handler_method.parameters.include?([:key,:uuid]) || handler_method.parameters.include?([:keyreq,:uuid]) + end + + def uses_named_arguments? + handler_method.parameters.include?([:req,:channel]) && handler_method.parameters.include?([:req,:message]) + end + + def handler_method + @handler_method ||= handler.method(:receive) end end end end - diff --git a/spec/lib/mantle/message_spec.rb b/spec/lib/mantle/message_spec.rb index 71776a4..06aa5df 100644 --- a/spec/lib/mantle/message_spec.rb +++ b/spec/lib/mantle/message_spec.rb @@ -7,6 +7,7 @@ catch_up = double("catch up") channel = "create:person" message = { id: 1 } + actual_message = message.merge(__MANTLE__: { sent_at: instance_of(Time) }) mantle_message = Mantle::Message.new(channel) mantle_message.message_bus = bus @@ -15,7 +16,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message) + mantle_message.publish(message: message) expect(bus).to have_received(:publish).with(channel, message) expect(catch_up).to have_received(:add_message).with(channel, message) @@ -27,7 +28,7 @@ catch_up = double("catch up") channel = "create:person" message = { id: 1 } - actual_message = message.merge(__MANTLE__: { message_source: 'SantaClaus' }) + actual_message = message.merge(__MANTLE__: { sent_at: instance_of(Time), message_source: 'SantaClaus' }) mantle_message = Mantle::Message.new(channel) mantle_message.message_bus = bus @@ -36,7 +37,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message) + mantle_message.publish(message: message) expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message) @@ -51,9 +52,9 @@ catch_up = double("catch up") channel = "create:person" message = { id: 1 } - actual_message = message.merge(__MANTLE__: { message_source: 'SantaClaus' }, external_payload: { external_store: :redis, uuid: instance_of(String) }) + actual_message = message.merge(__MANTLE__: { sent_at: instance_of(Time), message_source: 'SantaClaus', uuid: instance_of(String) }) - external_payload = { some: :really, huge: [ { payload: "containing", misc: "stuff" } ] } + payload = { some: :really, huge: [ { payload: "containing", misc: "stuff" } ] } mantle_message = Mantle::Message.new(channel) mantle_message.message_bus = bus @@ -62,7 +63,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message, external_payload: external_payload, external_store: :redis) + mantle_message.publish(message: message, payload: payload) expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message) From 6f44f484805d7b6d7f17eb7a8380c7022001cc7e Mon Sep 17 00:00:00 2001 From: Scott Gibson Date: Fri, 1 Apr 2022 09:58:28 -0400 Subject: [PATCH 16/16] allow legacy args --- lib/mantle/message.rb | 24 +++++++++++++++++++----- spec/lib/mantle/message_spec.rb | 10 +++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/mantle/message.rb b/lib/mantle/message.rb index 480ab87..8e85223 100644 --- a/lib/mantle/message.rb +++ b/lib/mantle/message.rb @@ -9,7 +9,25 @@ def initialize(channel) @catch_up = Mantle::CatchUp.new end - def publish(message: nil, payload: nil, expires_in: nil, keep_for: nil) + def method_missing(m, *args, &block) + raise if m.to_sym != :publish + + if (args.count == 1 && (args[0].keys - [:message, :mantle, :payload, :expires_in, :keep_for]).any?) + message = {} + args[0].keys.reject { |k| [:message, :mantle, :payload, :expires_in, :keep_for].include?(k) }.each { |k, v| message[k] = args[0].delete(k) } + args[0][:message] = message if message.any? + elsif (args.count == 2) + args[1][:message] = args.slice!(0) + end + + self.send(:_publish, *args, &block) + end + + private + + attr_reader :message_bus, :catch_up, :meta_data + + def _publish(message: nil, payload: nil, expires_in: nil, keep_for: nil) # Add __MANTLE__ meta-data... mantle_meta_data(sent_at: Time.now) mantle_meta_data(message_source: whoami) if whoami @@ -20,10 +38,6 @@ def publish(message: nil, payload: nil, expires_in: nil, keep_for: nil) catch_up.add_message(channel, message) end - private - - attr_reader :message_bus, :catch_up, :meta_data - def mantle_meta_data(meta_data) @meta_data ||= { } @meta_data.merge!(meta_data) diff --git a/spec/lib/mantle/message_spec.rb b/spec/lib/mantle/message_spec.rb index 06aa5df..d199d14 100644 --- a/spec/lib/mantle/message_spec.rb +++ b/spec/lib/mantle/message_spec.rb @@ -16,10 +16,10 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message: message) + mantle_message.publish(message) - expect(bus).to have_received(:publish).with(channel, message) - expect(catch_up).to have_received(:add_message).with(channel, message) + expect(bus).to have_received(:publish).with(channel, actual_message) + expect(catch_up).to have_received(:add_message).with(channel, actual_message) end it "published message includes message_source" do @@ -37,7 +37,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message: message) + mantle_message.publish(message) expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message) @@ -63,7 +63,7 @@ allow(bus).to receive(:publish) allow(catch_up).to receive(:add_message) - mantle_message.publish(message: message, payload: payload) + mantle_message.publish(message, payload: payload) expect(bus).to have_received(:publish).with(channel, actual_message) expect(catch_up).to have_received(:add_message).with(channel, actual_message)