{"id":20515786,"url":"https://github.com/kuper-tech/sbmt-outbox","last_synced_at":"2026-01-11T04:59:26.617Z","repository":{"id":230400999,"uuid":"757297513","full_name":"Kuper-Tech/sbmt-outbox","owner":"Kuper-Tech","description":"Transactional outbox pattern","archived":false,"fork":false,"pushed_at":"2025-04-03T13:35:21.000Z","size":10560,"stargazers_count":126,"open_issues_count":1,"forks_count":3,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-05-25T05:04:11.012Z","etag":null,"topics":["gem","inbox","outbox","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kuper-Tech.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-14T07:50:04.000Z","updated_at":"2025-04-09T08:53:49.000Z","dependencies_parsed_at":"2024-06-24T07:38:59.472Z","dependency_job_id":"e00f7264-12e5-4f6c-a1a1-78254440f093","html_url":"https://github.com/Kuper-Tech/sbmt-outbox","commit_stats":null,"previous_names":["sbermarket-tech/sbmt-outbox","kuper-tech/sbmt-outbox"],"tags_count":114,"template":false,"template_full_name":null,"purl":"pkg:github/Kuper-Tech/sbmt-outbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuper-Tech%2Fsbmt-outbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuper-Tech%2Fsbmt-outbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuper-Tech%2Fsbmt-outbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuper-Tech%2Fsbmt-outbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kuper-Tech","download_url":"https://codeload.github.com/Kuper-Tech/sbmt-outbox/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuper-Tech%2Fsbmt-outbox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279020904,"owners_count":26086948,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gem","inbox","outbox","ruby"],"created_at":"2024-11-15T21:24:41.242Z","updated_at":"2025-10-14T20:36:21.425Z","avatar_url":"https://github.com/Kuper-Tech.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Gem Version](https://badge.fury.io/rb/sbmt-outbox.svg)](https://badge.fury.io/rb/sbmt-outbox)\n[![Build Status](https://github.com/SberMarket-Tech/sbmt-outbox/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/SberMarket-Tech/sbmt-outbox/actions?query=branch%3Amaster)\n\n# Sbmt-Outbox\n\n\u003cimg src=\"https://raw.githubusercontent.com/SberMarket-Tech/sbmt-outbox/master/.github/outbox-logo.png\" alt=\"sbmt-outbox logo\" height=\"220\" align=\"right\" /\u003e\n\nMicroservices often publish messages after a transaction has been committed. Writing to the database and publishing a message are two separate transactions, so they must be atomic. A failed publication of a message could lead to a critical failure of the business process.\n\nThe Outbox pattern provides a reliable solution for message publishing. The idea behind this approach is to have an \"outgoing message table\" in the service's database. Before the main transaction completes, a new message row is added to this table. As a result, two actions take place as part of a single transaction. An asynchronous process retrieves new rows from the database table and, if they exist, publishes the messages to the broker.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"sbmt-outbox\"\n```\n\nAnd then execute:\n\n```shell\nbundle install\n```\n\n## Demo\n\nLearn how to use this gem and how it works with Ruby on Rails at here https://github.com/SberMarket-Tech/outbox-example-apps\n\n## Auto configuration\n\nWe recommend going through the configuration and files creation process using the following Rails generators:\n\nEach generator can be run by using the `--help` option to learn more about the available arguments.\n\n### Initial configuration\n\nIf you plug the gem into your application for the first time, you can generate the initial configuration:\n\n```shell\nrails g outbox:install\n```\n\n### Outbox/inbox items creation\n\nAn ActiveRecord model can be generated for the outbox/inbox item like this:\n\n```shell\nrails g outbox:item MaybeNamespaced::SomeOutboxItem --kind outbox\nrails g outbox:item MaybeNamespaced::SomeInboxItem --kind inbox\n```\n\nAs the result, a migration and a model will be created and the `outbox.yml` file configured.\n\n### Transport creation\n\nA transport is a class that is invoked while processing a specific outbox or inbox item. The transport must return either a boolean value or a dry monad result.\n\n```shell\nrails g outbox:transport MaybeNamespaced::SomeOutboxItem some/transport/name --kind outbox\nrails g outbox:transport MaybeNamespaced::SomeInboxItem some/transport/name --kind inbox\n```\n\n## Usage\n\nTo create an Outbox item, you should call the Interactor with the Item Model Class and `event_key` as arguments. The latter will be the Partitioning Key.\n\n```ruby\ntransaction do\n  some_record.save!\n\n  result = Sbmt::Outbox::CreateOutboxItem.call(\n    MyOutboxItem,\n    event_key: some_record.id,\n    attributes: {\n      payload: some_record.generate_payload,\n      options: {\n        key: some_record.id, # optional, may be used when producing to a Kafka topic\n        headers: {'FOO_BAR' =\u003e 'baz'} # optional, you can add custom headers\n      }\n    }\n  )\n\n  raise result.failure unless result.success?\nend\n```\n\nTo create multiple Outbox items in batch, you should call the Interactor with the Item Model Class and batch attributes, each item should have same list of keys. Each item should have `event_key` element, it will be the Partitioning Key.\n\n```ruby\ntransaction do\n  some_record.save!\n  another_record.save!\n\n  result = Sbmt::Outbox::CreateOutboxBatch.call(\n    MyOutboxItem,\n    batch_attributes: [\n      {\n        event_key: some_record.id,\n        payload: some_record.generate_payload,\n        options: {\n          key: some_record.id, # optional, may be used when producing to a Kafka topic\n          headers: {'FOO_BAR' =\u003e 'baz'} # optional, you can add custom headers\n        }\n      },\n      {\n        event_key: another_record.id,\n        payload: another_record.generate_payload,\n        options: {\n          key: another_record.id, # optional, may be used when producing to a Kafka topic\n          headers: {'FOO_BAR' =\u003e 'baz'} # optional, you can add custom headers\n        }\n      }\n    ]\n  )\n\n  raise result.failure unless result.success?\nend\n```\n\n## Monitoring\n\nWe use [Yabeda](https://github.com/yabeda-rb/yabeda) to collect [all kind of metrics](./config/initializers/yabeda.rb).\n\nExample of a Grafana dashboard that you can import [from a file](./examples/grafana-dashboard.json):\n\n![Grafana Dashboard](./examples/outbox-grafana-preview.png)\n\n[Full picture](./examples/outbox-grafana.png)\n\n## Manual configuration\n\n### `Outboxfile`\n\nFirst of all you should create an `Outboxfile` at the root of your application with the following code:\n\n```ruby\n# frozen_string_literal: true\n\nrequire_relative \"config/environment\"\n\n# Comment out this line if you don't want to use a metrics exporter\nYabeda::Prometheus::Exporter.start_metrics_server!\n```\n\n### `config/initializers/outbox.rb`\n\nThe `config/initializers/outbox.rb` file contains the overall general configuration.\n\n```ruby\n# config/initializers/outbox.rb\n\nRails.application.config.outbox.tap do |config|\n  config.redis = {url: ENV.fetch(\"REDIS_URL\")} # Redis is used as a coordinator service\n  config.paths \u003c\u003c Rails.root.join(\"config/outbox.yml\").to_s # optional; configuration file paths, deep merged at the application start, useful with Rails engines\n\n  # optional\n  config.poller = ActiveSupport::OrderedOptions.new.tap do |pc|\n    # max parallel threads (per box-item, globally)\n    pc.concurrency = 6\n    # max threads count (per worker process)\n    pc.threads_count = 1\n    # maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted\n    pc.general_timeout = 60\n    # poll buffer consists of regular items (errors_count = 0, i.e. without any processing errors) and retryable items (errors_count \u003e 0)\n    # max poll buffer size = regular_items_batch_size + retryable_items_batch_size\n    pc.regular_items_batch_size = 200\n    pc.retryable_items_batch_size = 100\n\n    # poll tactic: default is optimal for most cases: rate limit + redis job-queue size threshold\n    # poll tactic: aggressive is for high-intensity data: without rate limits + redis job-queue size threshold\n    # poll tactic: low-priority is for low-intensity data: rate limits + redis job-queue size threshold + + redis job-queue lag threshold\n    pc.tactic = \"default\"\n    # number of batches that one thread will process per rate interval\n    pc.rate_limit = 60\n    # rate interval in seconds\n    pc.rate_interval = 60\n    # mix / max redis job queue thresholds per box-item for default / aggressive / low-priority poll tactics\n    pc.min_queue_size = 10\n    pc.max_queue_size = 100\n    # min redis job queue time lag threshold per box-item for low-priority poll tactic (in seconds)\n    pc.min_queue_timelag = 5\n    # throttling delay for default / aggressive / low-priority poll tactics (in seconds)\n    pc.queue_delay = 0.1\n  end\n\n  # optional\n  config.processor = ActiveSupport::OrderedOptions.new.tap do |pc|\n    # max threads count (per worker process)\n    pc.threads_count = 4\n    # maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted\n    pc.general_timeout = 120\n    # BRPOP delay (in seconds) for polling redis job queue per box-item\n    pc.brpop_delay = 2\n  end\n```\n\n### Outbox pattern\n\nYou should create a database table in order for the process to view your outgoing messages.\n\n```ruby\ncreate_table :my_outbox_items do |t|\n  t.uuid :uuid, null: false\n  t.string :event_name, null: false # optional, use it when you have several events per one outbox table\n  t.string :event_key, null: false\n  t.integer :bucket, null: false\n  t.integer :status, null: false, default: 0\n  t.jsonb :options\n  t.binary :payload, null: false # when using mysql the column type should be mediumblob\n  t.integer :errors_count, null: false, default: 0\n  t.text :error_log\n  t.timestamp :processed_at\n  t.timestamps\nend\n\nadd_index :my_outbox_items, :uuid, unique: true\nadd_index :my_outbox_items, [:status, :id, :bucket], algorithm: :concurrently, include: [:errors_count]\nadd_index :my_outbox_items, [:event_name, :event_key, :id]\nadd_index :my_outbox_items, :created_at\n```\n\nYou can combine various types of messages within a single table. To do this, you should include an `event_name` field in the table. However, this approach is only justified if it is assumed that there won't be many events, and those events will follow the same retention and retry policy.\n\n```ruby\n# app/models/my_outbox_item.rb\nclass MyOutboxItem \u003c Sbmt::Outbox::OutboxItem\n  validates :event_name, presence: true # optional\nend\n```\n\n#### outbox.yml\nThe `outbox.yml` configuration file is the main configuration for the gem, where parameters for each outbox/inbox item are located.\n\n```yaml\n# config/outbox.yml\ndefault: \u0026default\n  owner: foo-team # optional, used in Yabeda metrics\n  bucket_size: 16 # optional, default 16, see into about the buckets at the #Concurrency section\n  metrics:\n    enabled: true # default false, yabeda server autostart with port: 9090 and path: /metrics\n    port: 9090 # optional, default, used in Yabeda metrics\n  probes:\n    enabled: false # optional, default true\n    port: 5555 # default, used for Kubernetes probes\n\n  outbox_items: # outbox items section\n    my_outbox_item: # underscored model class name\n      owner: my_outbox_item_team # optional, used in Yabeda metrics\n      retention: P1W #optional, default: P1W, for statuses: failed and discarded, retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations\n      min_retention_period: P1D #optional, default: P1D, for statuses: failed and discarded, retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations\n      retention_delivered_items: PT6H #optional, default: P1W, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n      delivered_min_retention_period: PT1H #optional, default: PT1H, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n      deletion_batch_size: 1_000 #optional, default: 1_000\n      deletion_sleep_time: 0.5 #optional, default: 0.5\n      deletion_time_window: PT4H #optional, default: PT4H, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n      max_retries: 3 # default 0, the number of retries before the item will be marked as failed\n      strict_order: false # optional, default\n      transports: # transports section\n        produce_message: # underscored transport class name\n          # transport reserved options\n          class: produce_message # optional; default is inferred from transport name\n          disposable: false # optional; default false; if true, the transport class will be instantiated only once\n          # ProduceMessage instance arguments\n          topic: \"my-topic-name\"\n\ndevelopment:\n  \u003c\u003c: *default\n\ntest:\n  \u003c\u003c: *default\n  bucket_size: 2\n\nproduction:\n  \u003c\u003c: *default\n  bucket_size: 256\n```\n__CAUTION__:\n- ⚠️ If this option is enabled and an error occurs while processing a message in a bucket,\nsubsequent messages in that bucket won't be processed until the current message is either skipped or successfully processed\n- ⚠️ Cannot use `retry_strategies` and the `strict_order` option at the same time\n\n```ruby\n# app/services/import_order.rb\nclass ProduceMessage\n  def initialize(topic:)\n    @topic = topic\n  end\n\n  def call(outbox_item, payload)\n    # send message to topic\n    true # mark message as processed\n  end\nend\n```\n\n**If you use Kafka as a transport, it is recommended that you use the [`sbmt-kafka_producer`](https://github.com/SberMarket-Tech/sbmt-kafka_producer) gem for this purpose.**\n\nTransports are defined as follows when `event_name` is used:\n\n```yaml\noutbox_items:\n  my_outbox_item:\n    transports:\n        # transport reserved options\n      - class: produce_message\n        event_name: \"order_created\" # event name marker\n        # ProduceMessage instance arguments\n        topic: \"order_created_topic\" # some transport argument\n      - class: produce_message\n        event_name: \"orders_completed\"\n        topic: \"orders_completed_topic\"\n```\n\n### Inbox pattern\n\nThe database migration will be the same as described in the Outbox pattern.\n\n```ruby\n# app/models/my_inbox_item.rb\nclass MyInboxItem \u003c Sbmt::Outbox::InboxItem\nend\n```\n\n```yaml\n# config/outbox.yml\n# see main configuration at the Outbox pattern\ninbox_items: # inbox items section\n  my_inbox_item: # underscored model class name\n    owner: my_inbox_item_team # optional, used in Yabeda metrics\n    retention: P1W #optional, default: P1W, for statuses: failed and discarded, retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations\n    min_retention_period: P1D #optional, default: P1D, for statuses: failed and discarded, retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations\n    retention_delivered_items: PT6H #optional, default: P1W, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n    delivered_min_retention_period: PT1H #optional, default: PT1H, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n    deletion_batch_size: 1_000 #optional, default: 1_000\n    deletion_sleep_time: 0.5 #optional, default: 0.5\n    deletion_time_window: PT4H #optional, default: PT4H, for statuses: delivered, retention period for delivered items, https://en.wikipedia.org/wiki/ISO_8601#Durations\n    max_retries: 3 # default 0, the number of retries before the item will be marked as failed\n    transports: # transports section\n      import_order: # underscored transport class name\n        source: \"kafka\" # default transport arguments\n```\n\n```ruby\n# app/services/import_order.rb\nclass ImportOrder\n  def initialize(source:)\n    @source = source\n  end\n\n  def call(outbox_item, payload)\n    # some work to create order in the database\n    true # mark message as processed\n  end\nend\n```\n\n**If you use Kafka, it is recommended that you use the [`sbmt-kafka_consumer`](https://github.com/SberMarket-Tech/sbmt-kafka_consumer) gem for this purpose.**\n\n### Retry strategies\n\nThe gem uses several types of retry strategies to repeat message processing if an error occurs. These strategies can be combined and will be executed one after the other. Each retry strategy takes one of three actions: to process the message, to skip processing the message or to skip processing and mark the message as \"skipped\" for future processing.\n\n#### Exponential backoff\n\nThis strategy periodically attempts to resend failed messages, with increasing delays in between each attempt.\n\n```yaml\n# config/outbox.yml\noutbox_items:\n  my_outbox_item:\n    ...\n    minimal_retry_interval: 10 # default: 10\n    maximal_retry_interval: 600 # default: 600\n    multiplier_retry_interval: 2 # default: 2\n    retry_strategies:\n      - exponential_backoff\n```\n\n#### Latest available\n\nThis strategy ensures idempotency. In short, if a message fails and a later message with the same event_key has already been delivered, then you most likely do not want to re-deliver the first one when it is retried.\n\n```yaml\n# config/outbox.yml\noutbox_items:\n  my_outbox_item:\n    ...\n    retry_strategies:\n      - exponential_backoff\n      - latest_available\n```\n\nThe exponential backoff strategy should be used in conjunction with the latest available strategy, and it should come last to minimize the number of database queries.\n\n### Partition strategies\n\nDepending on which type of data is used in the `event_key`, it is necessary to choose the right partitioning strategy.\n\n#### Number partitioning\n\nThis strategy should be used when the `event_key` field contains a number. For example, it could be `52523`, or `some-chars-123`. Any characters that aren't numbers will be removed, and only the numbers will remain. This strategy is used as a default.\n\n```yaml\n# config/outbox.yml\noutbox_items:\n  my_outbox_item:\n    ...\n    partition_strategy: number\n```\n\n#### Hash partitioning\n\nThis strategy should be used when the `event_key` is a string or uuid.\n\n```yaml\n# config/outbox.yml\noutbox_items:\n  my_outbox_item:\n    ...\n    partition_strategy: hash\n```\n\n## Rake tasks\n\n```shell\nrake outbox:delete_items\nrake outbox:update_status_items\n```\n\nExample run:\n```shell\nrake outbox:delete_items[OutboxItem,1] # Mandatory parameters box class and status\nrake outbox:update_status_items[OutboxItem,0,3] # Mandatory parameters box class, current status and new status\n\n```\n\nBoth tasks have optional parameters:\n```ruby\n- start_time # boxes are younger than the specified time, by default nil, time is specified in the format \"2025-01-05T23:59:59\"\n- end_time # boxes are older than the specified time, by default 6.hours.ago, time is specified in the format \"2025-01-05T23:59:59\"\n- batch_size # batch size, by default 1_000\n- sleep_time # sleep time between batches, by default 0.5\n```\n\nExample with optional parameters:\n  - format optional parameters:\n  ```shell\n    rake outbox:delete_items[klass_name,status,start_time,end_time,batch_size,sleep_time]\n\n    rake outbox:update_status_items[klass_name,status,new_status,start_time,end_time,batch_size,sleep_time]\n  ```\n  - example:\n  ```shell\n    rake outbox:delete_items[OutboxItem,1,\"2025-01-05T23:59:59\",\"2025-01-05T00:00:00\",10_000,5]\n\n    rake outbox:update_status_items[OutboxItem,0,3,\"2025-01-05T23:59:59\",\"2025-01-05T00:00:00\",10_000,5]\n  ```\n\n## Concurrency\n\nThe worker process consists of a poller and a processor, each of which has its own thread pool.\nThe poller is responsible for fetching messages ready for processing from the database table.\nThe processor, in turn, is used for their consistent processing (while preserving the order of messages and the partitioning key).\nEach bunch of buckets (i.e. buckets partition) is consistently fetched by poller one at a time. Each bucket is processed one at a time by a processor.\nA bucket is a number in a row in the `bucket` column generated by the partitioning strategy based on the `event_key` column when a message was committed to the database within the range of zero to `bucket_size`.\nThe number of bucket partitions, which poller uses is 6 by default. The number of poller threads is 2 by default and is not intended for customization.\nThe default number of processor threads is 4 and can be configured with the --concurrency option, thereby allowing you to customize message processing performance.\nThis architecture was designed to allow the daemons to scale without stopping the entire system in order to avoid mixing messages chronologically.\n\n### Middlewares\n\nYou can wrap item processing within middlewares. There are three types:\n- client middlewares – triggered outside of a daemon; executed alongside an item is created\n- server middlewares – triggered inside a daemon; divided into two types:\n  - batch middlewares – executed alongside a batch being fetched from the database\n  - item middlewares – execute alongside an item during processing\n  - polling middlewares - execute with element during pooling\n\nThe order of execution depends on the order specified in the outbox configuration:\n\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.item_process_middlewares.push(\n    'MyFirstItemMiddleware', # goes first\n    'MySecondItemMiddleware' # goes second\n  )\nend\n```\n\n#### Client middlewares\n\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.create_item_middlewares.push(\n    'MyCreateItemMiddleware'\n  )\n  config.create_batch_middlewares.push(\n    'MyCreateBatchMiddleware'\n  )\nend\n\n# my_create_item_middleware.rb\nclass MyCreateItemMiddleware\n  def call(item_class, item_attributes)\n    # your code\n    yield\n    # your code\n  end\nend\n\n# my_create_batch_middleware.rb\nclass MyCreateBatchMiddleware\n  def call(item_class, batch_attributes)\n    # your code\n    yield\n    # your code\n  end\nend\n```\n\n#### Server middlewares\n\nExample of a batch middleware:\n\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.batch_process_middlewares.push(\n    'MyBatchMiddleware'\n  )\nend\n\n# my_batch_middleware.rb\nclass MyBatchMiddleware\n  def call(job)\n    # your code\n    yield\n    # your code\n  end\nend\n```\n\nExample of an item middleware:\n\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.item_process_middlewares.push(\n    'MyItemMiddleware'\n  )\nend\n\n# my_create_item_middleware.rb\nclass MyItemMiddleware\n  def call(item)\n    # your code\n    yield\n    # your code\n  end\nend\n```\n\nExample of an polling middleware:\n\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.polling_item_middlewares.push(\n    'MyItemMiddleware'\n  )\nend\n\n# my_create_polling_middleware.rb\nclass MyPollingItemMiddleware\n  def call(item)\n    # your code\n    yield\n    # your code\n  end\nend\n\n## Tracing\n\nThe gem is optionally integrated with OpenTelemetry. If your main application has `opentelemetry-*` gems, the tracing will be configured automatically.\n\n## Web UI\n\nOutbox comes with a [Ract web application](https://github.com/SberMarket-Tech/sbmt-outbox-ui) that can list existing outbox and inbox models.\n\n```ruby\nRails.application.routes.draw do\n  mount Sbmt::Outbox::Engine =\u003e \"/outbox-ui\"\nend\n```\n\n**The path `/outbox-ui` cannot be changed for now**\n\nUnder the hood it uses a React application provided as [npm package](https://www.npmjs.com/package/sbmt-outbox-ui).\n\nBy default, the npm packages is served from `https://cdn.jsdelivr.net/npm/sbmt-outbox-ui@x.y.z/dist/assets/index.js`. It could be changed by the following config option:\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.cdn_url = \"https://some-cdn-url\"\nend\n```\n\n### UI development\n\nIf you want to implement some features for Outbox UI, you can serve javascript locally like the following:\n1. Start React application by `npm run dev`\n2. Configure Outbox to serve UI scripts locally:\n```ruby\n# config/initializers/outbox.rb\nRails.application.config.outbox.tap do |config|\n  config.ui.serve_local = true\nend\n```\n\nWe would like to see more features added to the web UI. If you have any suggestions, please feel free to submit a pull request 🤗.\n\n## CLI Arguments\n\n| Key                        | Description                                                          |\n|----------------------------|----------------------------------------------------------------------|\n| `--boxes or -b`            | Outbox/Inbox processors to start`                                    |\n| `--concurrency or -c`      | Number of process threads. Default 4.                                |\n| `--poll-concurrency or -p` | Number of poller partitions. Default 6.                              |\n| `--poll-threads or -n`     | Number of poll threads. Default 1.                                   |\n| `--poll-tactic or -t`      | Poll tactic. Default \"default\".                                      |\n\n## Development \u0026 Test\n\n### Installation\n\n- Install [Dip](https://github.com/bibendi/dip)\n- Run `dip provision`\n\n### Usage\n\n- Run `dip setup`\n- Run `dip test`\n\nSee more commands at [dip.yml](./dip.yml).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuper-tech%2Fsbmt-outbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkuper-tech%2Fsbmt-outbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuper-tech%2Fsbmt-outbox/lists"}