tobox 0.4.5 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +105 -47
- data/lib/tobox/cli.rb +7 -1
- data/lib/tobox/configuration.rb +11 -8
- data/lib/tobox/fetcher.rb +78 -81
- data/lib/tobox/plugins/event_grouping.rb +47 -0
- data/lib/tobox/plugins/inbox.rb +46 -0
- data/lib/tobox/plugins/progress.rb +33 -0
- data/lib/tobox/plugins/stats.rb +23 -4
- data/lib/tobox/pool/fiber_pool.rb +50 -10
- data/lib/tobox/pool/threaded_pool.rb +22 -11
- data/lib/tobox/pool.rb +2 -2
- data/lib/tobox/version.rb +1 -1
- data/lib/tobox/worker.rb +1 -1
- data/lib/tobox.rb +3 -1
- metadata +7 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 15d95687a102c98fcc33859b3a369dc9026f04fa58f7f785bcad1d9ccb40010f
         | 
| 4 | 
            +
              data.tar.gz: 656b296f9d72d67877945317d4a6d5505de63044f74e08c506970b2c841599f4
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 172b54fd340f865dbc26a30266f815fe556424cd150c8205c872da02d3725def786648710097b0880de41cc16940a39bd34c8592049d22a907d527ff84b26a3a
         | 
| 7 | 
            +
              data.tar.gz: 1dafbf50e8d18c4eb7f4dfd31539322230bd1d11d403f250e4612a0579e878c366fe6b2d44eb24ae17f32476bd25994457563d3fd5ef1a46d7499dc92a93ee1d
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,29 @@ | |
| 1 1 | 
             
            ## [Unreleased]
         | 
| 2 2 |  | 
| 3 | 
            +
            ## [0.5.1] - 2024-09-26
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ### Improvements
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            * Refactoring of management of event id which replaces `SELECT id IN (?)` resulting queries with `SELECT id = ?`.
         | 
| 8 | 
            +
            * Process shutdown is now more predictable, also in the grace period.
         | 
| 9 | 
            +
              * `grace_shutdown_timeout` is a new configuration, by default 5 (seconds).
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ## [0.5.0] - 2024-09-16
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ### Features
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            A new `:progress` plugin can be used in order to release database transactions before handling events (useful for when event handling times vary and may cause transaction bookkeeping overhead in the database).
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            **Note**: This may become the default behaviour in a future release.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            ### Improvements
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            * The event grouping and inbox capabilities were converted into plugins (respectively, `:event_grouping` and `:inbox`).
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ### Bugfixes
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            * exponential backoff calculation was broken.
         | 
| 26 | 
            +
            * behaviour fixed for databases which do not support `ON CONFLICT` or `UPDATE ... RETURNING` (like MySQL).
         | 
| 3 27 |  | 
| 4 28 | 
             
            ## [0.4.5] - 2024-02-28
         | 
| 5 29 |  | 
    
        data/README.md
    CHANGED
    
    | @@ -13,10 +13,10 @@ Simple, data-first events processing framework based on the [transactional outbo | |
| 13 13 | 
             
            - [Usage](#usage)
         | 
| 14 14 | 
             
            - [Configuration](#configuration)
         | 
| 15 15 | 
             
            - [Event](#event)
         | 
| 16 | 
            -
            - [Features](#features)
         | 
| 17 | 
            -
              - [Ordered event processing](#ordered-event-processing)
         | 
| 18 | 
            -
              - [Inbox](#inbox)
         | 
| 19 16 | 
             
            - [Plugins](#plugins)
         | 
| 17 | 
            +
              - [Progress](#progress)
         | 
| 18 | 
            +
              - [Event Grouping](#event-grouping)
         | 
| 19 | 
            +
              - [Inbox](#inbox)
         | 
| 20 20 | 
             
              - [Zeitwerk](#zeitwerk)
         | 
| 21 21 | 
             
              - [Sentry](#sentry)
         | 
| 22 22 | 
             
              - [Datadog](#datadog)
         | 
| @@ -204,7 +204,17 @@ table :outbox | |
| 204 204 | 
             
            Maximum number of times a failed attempt to process an event will be retried (`10` by default).
         | 
| 205 205 |  | 
| 206 206 | 
             
            ```ruby
         | 
| 207 | 
            -
             | 
| 207 | 
            +
            max_attempts 4
         | 
| 208 | 
            +
            ```
         | 
| 209 | 
            +
             | 
| 210 | 
            +
            **Note**: the new attempt will be retried in `n ** 4`, where `n` is the number of past attempts for that event.
         | 
| 211 | 
            +
             | 
| 212 | 
            +
            ### `exponential_retry_factor`
         | 
| 213 | 
            +
             | 
| 214 | 
            +
            Factor by which the number of seconds until an event can be retried will be exponentially calculated, i.e. 2 seconds on first attempt, then 4, then 8, then 16 (`2` by default).
         | 
| 215 | 
            +
             | 
| 216 | 
            +
            ```ruby
         | 
| 217 | 
            +
            exponential_retry_factor 2
         | 
| 208 218 | 
             
            ```
         | 
| 209 219 |  | 
| 210 220 | 
             
            **Note**: the new attempt will be retried in `n ** 4`, where `n` is the number of past attempts for that event.
         | 
| @@ -231,6 +241,10 @@ Time (in seconds) to wait before checking again for events in the outbox. | |
| 231 241 |  | 
| 232 242 | 
             
            Time (in seconds) to wait for events to finishing processing, before hard-killing the process.
         | 
| 233 243 |  | 
| 244 | 
            +
            ### `grace_shutdown_timeout`
         | 
| 245 | 
            +
             | 
| 246 | 
            +
            Grace period (in seconds) to wait after, hard-killing the work in progress, and before exiting the process.
         | 
| 247 | 
            +
             | 
| 234 248 | 
             
            ### `on(event_type) { |before, after| }`
         | 
| 235 249 |  | 
| 236 250 | 
             
            callback executed when processing an event of the given type. By default, it'll yield the state of data before and after the event (unless `message_to_arguments` is set).
         | 
| @@ -320,17 +334,6 @@ Overrides the internal logger (an instance of `Logger`). | |
| 320 334 |  | 
| 321 335 | 
             
            Overrides the default log level ("info" when in "production" environment, "debug" otherwise).
         | 
| 322 336 |  | 
| 323 | 
            -
            ### group_column
         | 
| 324 | 
            -
             | 
| 325 | 
            -
            Defines the column to be used for event grouping, when [ordered processing of events is a requirement](#ordered-event-processing).
         | 
| 326 | 
            -
             | 
| 327 | 
            -
            ### inbox table
         | 
| 328 | 
            -
             | 
| 329 | 
            -
            Defines the name of the table to be used for inbox, when [inbox usage is a requirement](#inbox).
         | 
| 330 | 
            -
             | 
| 331 | 
            -
            ### inbox column
         | 
| 332 | 
            -
             | 
| 333 | 
            -
            Defines the column in the outbox table which references the inbox table, when one is set.
         | 
| 334 337 |  | 
| 335 338 | 
             
            <a id="markdown-event" name="event"></a>
         | 
| 336 339 | 
             
            ## Event
         | 
| @@ -345,36 +348,85 @@ The event is composed of the following properties: | |
| 345 348 |  | 
| 346 349 | 
             
            (*NOTE*: The event is also composed of other properties which are only relevant for `tobox`.)
         | 
| 347 350 |  | 
| 348 | 
            -
            <a id="markdown-features" name="features"></a>
         | 
| 349 | 
            -
            ## Features
         | 
| 350 351 |  | 
| 351 | 
            -
             | 
| 352 | 
            +
            <a id="markdown-plugins" name="plugins"></a>
         | 
| 353 | 
            +
            ## Plugins
         | 
| 354 | 
            +
             | 
| 355 | 
            +
            `tobox` ships with a very simple plugin system. (TODO: add docs).
         | 
| 356 | 
            +
             | 
| 357 | 
            +
            Plugins can be loaded in the config via `plugin`:
         | 
| 358 | 
            +
             | 
| 359 | 
            +
            ```ruby
         | 
| 360 | 
            +
            # tobox.rb
         | 
| 361 | 
            +
            plugin(:plugin_name)
         | 
| 362 | 
            +
            ```
         | 
| 363 | 
            +
             | 
| 364 | 
            +
            <a id="markdown-progress" name="progress"></a>
         | 
| 365 | 
            +
            ### Progress
         | 
| 366 | 
            +
             | 
| 367 | 
            +
            By default, the database transaction used to consume the event is kept open while the event is handled. While this ensures atomic event consumption, it may also cause overhead related to transaction management given enough load, particularly in cases where event handling time varies (i.e. throttled HTTP requests).
         | 
| 368 | 
            +
             | 
| 369 | 
            +
            The `:progress` plugin fixes this by releasing the databaase transaction after fetching the event. It does so by making the fetched event "invisible" for a certain period, during which the event must be successfully handled.
         | 
| 370 | 
            +
             | 
| 371 | 
            +
            Here's how to use it:
         | 
| 372 | 
            +
             | 
| 373 | 
            +
            ```ruby
         | 
| 374 | 
            +
            # in your tobox.rb
         | 
| 375 | 
            +
            plugin :progress
         | 
| 376 | 
            +
             | 
| 377 | 
            +
            visibility_timeout 90 # default: 30
         | 
| 378 | 
            +
            ```
         | 
| 379 | 
            +
             | 
| 380 | 
            +
            3. insert related outbox events with the same group id
         | 
| 381 | 
            +
             | 
| 382 | 
            +
            ```ruby
         | 
| 383 | 
            +
            order = Order.new(
         | 
| 384 | 
            +
              item_id: item.id,
         | 
| 385 | 
            +
              price: 20_20,
         | 
| 386 | 
            +
              currency: "EUR"
         | 
| 387 | 
            +
            )
         | 
| 388 | 
            +
            DB.transaction do
         | 
| 389 | 
            +
              order.save
         | 
| 390 | 
            +
              DB[:outbox].insert(event_type: "order_created", group_id: order.id, data_after: order.to_hash)
         | 
| 391 | 
            +
              DB[:outbox].insert(event_type: "billing_event_started", group_id: order.id, data_after: order.to_hash)
         | 
| 392 | 
            +
            end
         | 
| 393 | 
            +
             | 
| 394 | 
            +
            # "order_created" will be processed first
         | 
| 395 | 
            +
            # "billing_event_created" will only start processing once "order_created" finishes
         | 
| 396 | 
            +
            ```
         | 
| 397 | 
            +
             | 
| 398 | 
            +
            #### Configuration
         | 
| 399 | 
            +
             | 
| 400 | 
            +
            ##### `visibility_timeout`
         | 
| 401 | 
            +
             | 
| 402 | 
            +
            Timeout (in seconds) after which a previously marked-for-consumption event can be retried (default: `30`)
         | 
| 352 403 |  | 
| 353 | 
            -
            <a id="markdown- | 
| 354 | 
            -
            ###  | 
| 404 | 
            +
            <a id="markdown-event-grouping" name="event-grouping"></a>
         | 
| 405 | 
            +
            ### Event grouping
         | 
| 355 406 |  | 
| 356 407 | 
             
            By default, events are taken and processed from the "outbox" table concurrently by workers, which means that, while worker A may process the most recent event, and worker B takes the following, worker B may process it faster than worker A. This may be an issue if the consumer expects events from a certain context to arrive in a certain order.
         | 
| 357 408 |  | 
| 358 | 
            -
            One solution is to have a single worker processing the "outbox" events. Another is to use the ` | 
| 409 | 
            +
            One solution is to have a single worker processing the "outbox" events. Another is to use the `:event_grouping` plugin.
         | 
| 359 410 |  | 
| 360 | 
            -
             | 
| 411 | 
            +
            All you have to do is:
         | 
| 361 412 |  | 
| 362 413 | 
             
            1. add a "group id" column to the "outbox" table
         | 
| 363 414 |  | 
| 364 415 | 
             
            ```ruby
         | 
| 365 416 | 
             
            create_table(:outbox) do
         | 
| 366 417 | 
             
              primary_key :id
         | 
| 367 | 
            -
              column :group_id, :integer
         | 
| 368 | 
            -
              # The type is irrelevant, could also be :string, :uuid...
         | 
| 418 | 
            +
              column :group_id, :integer # The type is irrelevant, could also be :string, :uuid...
         | 
| 369 419 | 
             
              # ..
         | 
| 420 | 
            +
              index :group_id
         | 
| 370 421 | 
             
            ```
         | 
| 371 422 |  | 
| 372 | 
            -
            2.  | 
| 423 | 
            +
            2. Enable the plugin
         | 
| 373 424 |  | 
| 374 425 | 
             
            ```ruby
         | 
| 375 426 | 
             
            # in your tobox.rb
         | 
| 376 | 
            -
             | 
| 377 | 
            -
             | 
| 427 | 
            +
            plugin :event_grouping
         | 
| 428 | 
            +
             | 
| 429 | 
            +
            group_column :group_id # by default already `:group_id`
         | 
| 378 430 | 
             
            ```
         | 
| 379 431 |  | 
| 380 432 | 
             
            3. insert related outbox events with the same group id
         | 
| @@ -394,25 +446,28 @@ end | |
| 394 446 | 
             
            # "order_created" will be processed first
         | 
| 395 447 | 
             
            # "billing_event_created" will only start processing once "order_created" finishes
         | 
| 396 448 | 
             
            ```
         | 
| 449 | 
            +
             | 
| 450 | 
            +
            #### Configuration
         | 
| 451 | 
            +
             | 
| 452 | 
            +
            ##### `group_column`
         | 
| 453 | 
            +
             | 
| 454 | 
            +
            Defines the database column to be used for event grouping (`:group_id` by default).
         | 
| 455 | 
            +
             | 
| 397 456 | 
             
            <a id="inbox" name="inbox"></a>
         | 
| 398 457 | 
             
            ### Inbox
         | 
| 399 458 |  | 
| 400 | 
            -
            `tobox` also supports the [inbox pattern](https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/), to ensure "exactly-once" processing of events. This is achieved by "tagging" events with a unique identifier, and registering them in the inbox before processing (and if they're there, ignoring it altogether).
         | 
| 459 | 
            +
            Via the `:inbox` plugin, `tobox` also supports the [inbox pattern](https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/), to ensure "exactly-once" processing of events. This is achieved by "tagging" events with a unique identifier, and registering them in the inbox before processing (and if they're there, ignoring it altogether).
         | 
| 401 460 |  | 
| 402 461 | 
             
            In order to do so, you'll have to:
         | 
| 403 462 |  | 
| 404 | 
            -
            1. add an "inbox" table in the database
         | 
| 463 | 
            +
            1. add an "inbox" table in the database and the unique id reference in the outbox table:
         | 
| 405 464 |  | 
| 406 465 | 
             
            ```ruby
         | 
| 407 466 | 
             
            create_table(:inbox) do
         | 
| 408 467 | 
             
              column :inbox_id, :varchar, null: true, primary_key: true # it can also be a uuid, you decide
         | 
| 409 468 | 
             
              column :created_at, "timestamp without time zone", null: false, default: Sequel::CURRENT_TIMESTAMP
         | 
| 410 469 | 
             
            end
         | 
| 411 | 
            -
            ```
         | 
| 412 | 
            -
             | 
| 413 | 
            -
            2. add the unique id reference in the outbox table:
         | 
| 414 470 |  | 
| 415 | 
            -
            ```ruby
         | 
| 416 471 | 
             
            create_table(:outbox) do
         | 
| 417 472 | 
             
              primary_key :id
         | 
| 418 473 | 
             
              column :type, :varchar, null: false
         | 
| @@ -421,15 +476,17 @@ create_table(:outbox) do | |
| 421 476 | 
             
              foreign_key :inbox_id, :inbox
         | 
| 422 477 | 
             
            ```
         | 
| 423 478 |  | 
| 424 | 
            -
             | 
| 479 | 
            +
            2. Load the plugin and reference them in the configuration
         | 
| 425 480 |  | 
| 426 481 | 
             
            ```ruby
         | 
| 427 482 | 
             
            # tobox.rb
         | 
| 428 | 
            -
             | 
| 429 | 
            -
             | 
| 483 | 
            +
            plugin :inbox
         | 
| 484 | 
            +
             | 
| 485 | 
            +
            inbox_table :inbox # :inbox by default already
         | 
| 486 | 
            +
            inbox_column :inbox_id # :inbox_id by default already
         | 
| 430 487 | 
             
            ```
         | 
| 431 488 |  | 
| 432 | 
            -
             | 
| 489 | 
            +
            3. insert related outbox events with an inbox id
         | 
| 433 490 |  | 
| 434 491 | 
             
            ```ruby
         | 
| 435 492 | 
             
            order = Order.new(
         | 
| @@ -446,19 +503,19 @@ end | |
| 446 503 | 
             
            # assuming this bit above runs two times in two separate workers, each will be processed by tobox only once.
         | 
| 447 504 | 
             
            ```
         | 
| 448 505 |  | 
| 449 | 
            -
             | 
| 506 | 
            +
            #### Configuration
         | 
| 450 507 |  | 
| 451 | 
            -
            <a id="markdown-plugins" name="plugins"></a>
         | 
| 452 | 
            -
            ## Plugins
         | 
| 453 508 |  | 
| 454 | 
            -
             | 
| 509 | 
            +
            ##### inbox table
         | 
| 455 510 |  | 
| 456 | 
            -
             | 
| 511 | 
            +
            Defines the name of the table to be used for inbox (`:inbox` by default).
         | 
| 457 512 |  | 
| 458 | 
            -
             | 
| 459 | 
            -
             | 
| 460 | 
            -
             | 
| 461 | 
            -
             | 
| 513 | 
            +
            ##### inbox column
         | 
| 514 | 
            +
             | 
| 515 | 
            +
            Defines the column in the outbox table which references the inbox table (`:inbox_id` by default).
         | 
| 516 | 
            +
             | 
| 517 | 
            +
             | 
| 518 | 
            +
            **NOTE**: make sure you keep cleaning the inbox periodically from older messages, once there's no more danger of receiving them again.
         | 
| 462 519 |  | 
| 463 520 | 
             
            It ships with the following integrations.
         | 
| 464 521 |  | 
| @@ -530,6 +587,7 @@ on_stats(5) do |stats_collector| # every 5 seconds | |
| 530 587 | 
             
              # now you can send them to your statsd collector
         | 
| 531 588 | 
             
              #
         | 
| 532 589 | 
             
              StatsD.gauge('outbox_pending_backlog', stats[:pending_count])
         | 
| 590 | 
            +
              StatsD.gauge('outbox_oldest_message_age', stats[:oldest_event_age_in_seconds])
         | 
| 533 591 | 
             
            end
         | 
| 534 592 | 
             
            ```
         | 
| 535 593 |  | 
| @@ -576,7 +634,7 @@ end | |
| 576 634 | 
             
            <a id="markdown-supported-rubies" name="supported-rubies"></a>
         | 
| 577 635 | 
             
            ## Supported Rubies
         | 
| 578 636 |  | 
| 579 | 
            -
            All Rubies greater or equal to 2. | 
| 637 | 
            +
            All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
         | 
| 580 638 |  | 
| 581 639 |  | 
| 582 640 | 
             
            <a id="markdown-rails-support" name="rails-support"></a>
         | 
    
        data/lib/tobox/cli.rb
    CHANGED
    
    | @@ -108,12 +108,18 @@ module Tobox | |
| 108 108 | 
             
                      opts[:tag] = arg
         | 
| 109 109 | 
             
                    end
         | 
| 110 110 |  | 
| 111 | 
            -
                    o.on "-t", "--shutdown-timeout NUM",  | 
| 111 | 
            +
                    o.on "-t", "--shutdown-timeout NUM", Float, "Shutdown timeout (in seconds)" do |arg|
         | 
| 112 112 | 
             
                      raise ArgumentError, "must be positive" unless arg.positive?
         | 
| 113 113 |  | 
| 114 114 | 
             
                      opts[:shutdown_timeout] = arg
         | 
| 115 115 | 
             
                    end
         | 
| 116 116 |  | 
| 117 | 
            +
                    o.on "-g", "--shutdown-grace-timeout NUM", Float, "Shutdown grace timeout (in seconds)" do |arg|
         | 
| 118 | 
            +
                      raise ArgumentError, "must be positive" unless arg.positive?
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                      opts[:grace_shutdown_timeout] = arg
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 117 123 | 
             
                    o.on "--verbose", "Print more verbose output" do |arg|
         | 
| 118 124 | 
             
                      opts[:verbose] = arg
         | 
| 119 125 | 
             
                    end
         | 
    
        data/lib/tobox/configuration.rb
    CHANGED
    
    | @@ -7,7 +7,8 @@ module Tobox | |
| 7 7 | 
             
              class Configuration
         | 
| 8 8 | 
             
                extend Forwardable
         | 
| 9 9 |  | 
| 10 | 
            -
                attr_reader :handlers, :lifecycle_events, :arguments_handler, :default_logger, :database
         | 
| 10 | 
            +
                attr_reader :plugins, :handlers, :lifecycle_events, :arguments_handler, :default_logger, :database, :fetcher_class,
         | 
| 11 | 
            +
                            :config
         | 
| 11 12 |  | 
| 12 13 | 
             
                def_delegator :@config, :[]
         | 
| 13 14 |  | 
| @@ -18,13 +19,12 @@ module Tobox | |
| 18 19 | 
             
                  database_uri: nil,
         | 
| 19 20 | 
             
                  database_options: nil,
         | 
| 20 21 | 
             
                  table: :outbox,
         | 
| 21 | 
            -
                   | 
| 22 | 
            -
                  inbox_table: nil,
         | 
| 23 | 
            -
                  inbox_column: nil,
         | 
| 22 | 
            +
                  created_at_column: nil,
         | 
| 24 23 | 
             
                  max_attempts: 10,
         | 
| 25 | 
            -
                  exponential_retry_factor:  | 
| 24 | 
            +
                  exponential_retry_factor: 2,
         | 
| 26 25 | 
             
                  wait_for_events_delay: 5,
         | 
| 27 26 | 
             
                  shutdown_timeout: 10,
         | 
| 27 | 
            +
                  grace_shutdown_timeout: 5,
         | 
| 28 28 | 
             
                  concurrency: 4, # TODO: CPU count
         | 
| 29 29 | 
             
                  worker: :thread
         | 
| 30 30 | 
             
                }.freeze
         | 
| @@ -45,6 +45,7 @@ module Tobox | |
| 45 45 | 
             
                  @handlers = {}
         | 
| 46 46 | 
             
                  @message_to_arguments = nil
         | 
| 47 47 | 
             
                  @plugins = []
         | 
| 48 | 
            +
                  @fetcher_class = Class.new(Fetcher)
         | 
| 48 49 |  | 
| 49 50 | 
             
                  if block
         | 
| 50 51 | 
             
                    case block.arity
         | 
| @@ -141,6 +142,8 @@ module Tobox | |
| 141 142 |  | 
| 142 143 | 
             
                  extend(plugin::ConfigurationMethods) if defined?(plugin::ConfigurationMethods)
         | 
| 143 144 |  | 
| 145 | 
            +
                  @fetcher_class.__send__(:include, plugin::FetcherMethods) if defined?(plugin::FetcherMethods)
         | 
| 146 | 
            +
             | 
| 144 147 | 
             
                  plugin.configure(self, **options, &block) if plugin.respond_to?(:configure)
         | 
| 145 148 | 
             
                end
         | 
| 146 149 |  | 
| @@ -157,7 +160,7 @@ module Tobox | |
| 157 160 | 
             
                private
         | 
| 158 161 |  | 
| 159 162 | 
             
                def method_missing(meth, *args, &block)
         | 
| 160 | 
            -
                  if  | 
| 163 | 
            +
                  if @config.key?(meth) && args.size == 1
         | 
| 161 164 | 
             
                    @config[meth] = args.first
         | 
| 162 165 | 
             
                  elsif /\Aon_(.*)\z/.match(meth) && args.empty?
         | 
| 163 166 | 
             
                    on(Regexp.last_match(1).to_sym, &block)
         | 
| @@ -167,8 +170,8 @@ module Tobox | |
| 167 170 | 
             
                end
         | 
| 168 171 |  | 
| 169 172 | 
             
                def respond_to_missing?(meth, *args)
         | 
| 170 | 
            -
                  super | 
| 171 | 
            -
                     | 
| 173 | 
            +
                  super ||
         | 
| 174 | 
            +
                    @config.key?(meth) ||
         | 
| 172 175 | 
             
                    /\Aon_(.*)\z/.match(meth)
         | 
| 173 176 | 
             
                end
         | 
| 174 177 | 
             
              end
         | 
    
        data/lib/tobox/fetcher.rb
    CHANGED
    
    | @@ -13,14 +13,10 @@ module Tobox | |
| 13 13 | 
             
                  @db = configuration.database
         | 
| 14 14 |  | 
| 15 15 | 
             
                  @table = configuration[:table]
         | 
| 16 | 
            -
                  @group_column = configuration[:group_column]
         | 
| 17 16 | 
             
                  @exponential_retry_factor = configuration[:exponential_retry_factor]
         | 
| 18 17 |  | 
| 19 18 | 
             
                  max_attempts = configuration[:max_attempts]
         | 
| 20 19 |  | 
| 21 | 
            -
                  @inbox_table = configuration[:inbox_table]
         | 
| 22 | 
            -
                  @inbox_column = configuration[:inbox_column]
         | 
| 23 | 
            -
             | 
| 24 20 | 
             
                  @ds = @db[@table]
         | 
| 25 21 |  | 
| 26 22 | 
             
                  run_at_conds = [
         | 
| @@ -32,6 +28,8 @@ module Tobox | |
| 32 28 | 
             
                                      .where(run_at_conds)
         | 
| 33 29 | 
             
                                      .order(Sequel.desc(:run_at, nulls: :first), :id)
         | 
| 34 30 |  | 
| 31 | 
            +
                  @mark_as_fetched_params = { attempts: Sequel[@table][:attempts] + 1, last_error: nil }
         | 
| 32 | 
            +
             | 
| 35 33 | 
             
                  @before_event_handlers = Array(@configuration.lifecycle_events[:before_event])
         | 
| 36 34 | 
             
                  @after_event_handlers = Array(@configuration.lifecycle_events[:after_event])
         | 
| 37 35 | 
             
                  @error_event_handlers = Array(@configuration.lifecycle_events[:error_event])
         | 
| @@ -39,73 +37,19 @@ module Tobox | |
| 39 37 |  | 
| 40 38 | 
             
                def fetch_events(&blk)
         | 
| 41 39 | 
             
                  num_events = 0
         | 
| 42 | 
            -
                   | 
| 43 | 
            -
                     | 
| 44 | 
            -
                      group = @pick_next_sql.for_update
         | 
| 45 | 
            -
                                            .skip_locked
         | 
| 46 | 
            -
                                            .limit(1)
         | 
| 47 | 
            -
                                            .select(@group_column)
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                      # get total from a group, to compare to the number of future locked rows.
         | 
| 50 | 
            -
                      total_from_group = @ds.where(@group_column => group).count
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                      event_ids = @ds.where(@group_column => group)
         | 
| 53 | 
            -
                                     .order(Sequel.desc(:run_at, nulls: :first), :id)
         | 
| 54 | 
            -
                                     .for_update.skip_locked.select_map(:id)
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                      if event_ids.size != total_from_group
         | 
| 57 | 
            -
                        # this happens if concurrent workers locked different rows from the same group,
         | 
| 58 | 
            -
                        # or when new rows from a given group have been inserted after the lock has been
         | 
| 59 | 
            -
                        # acquired
         | 
| 60 | 
            -
                        event_ids = []
         | 
| 61 | 
            -
                      end
         | 
| 62 | 
            -
             | 
| 63 | 
            -
                      # lock all, process 1
         | 
| 64 | 
            -
                      event_ids = event_ids[0, 1]
         | 
| 65 | 
            -
                    else
         | 
| 66 | 
            -
                      event_ids = @pick_next_sql.for_update
         | 
| 67 | 
            -
                                                .skip_locked
         | 
| 68 | 
            -
                                                .limit(1).select_map(:id) # lock starts here
         | 
| 69 | 
            -
                    end
         | 
| 40 | 
            +
                  events_tr do
         | 
| 41 | 
            +
                    event_id = nil
         | 
| 70 42 |  | 
| 71 | 
            -
                     | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
                      @db.transaction(savepoint: true) do
         | 
| 75 | 
            -
                        events = @ds.where(id: event_ids).returning.delete
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                        if blk
         | 
| 78 | 
            -
                          num_events = events.size
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                          events.map! do |ev|
         | 
| 81 | 
            -
                            try_insert_inbox(ev) do
         | 
| 82 | 
            -
                              ev[:metadata] = try_json_parse(ev[:metadata])
         | 
| 83 | 
            -
                              handle_before_event(ev)
         | 
| 84 | 
            -
                              yield(to_message(ev))
         | 
| 85 | 
            -
                              ev
         | 
| 86 | 
            -
                            end
         | 
| 87 | 
            -
                          rescue StandardError => e
         | 
| 88 | 
            -
                            error = e
         | 
| 89 | 
            -
                            raise Sequel::Rollback
         | 
| 90 | 
            -
                          end.compact!
         | 
| 91 | 
            -
                        else
         | 
| 92 | 
            -
                          events.map!(&method(:to_message))
         | 
| 93 | 
            -
                        end
         | 
| 94 | 
            -
                      end
         | 
| 43 | 
            +
                    event_id_tr do
         | 
| 44 | 
            +
                      event_id = fetch_event_id
         | 
| 45 | 
            +
                      mark_as_fetched(event_id) if event_id
         | 
| 95 46 | 
             
                    end
         | 
| 96 47 |  | 
| 97 | 
            -
                     | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 48 | 
            +
                    if event_id
         | 
| 49 | 
            +
                      with_event(event_id) do |event|
         | 
| 50 | 
            +
                        num_events = 1
         | 
| 100 51 |  | 
| 101 | 
            -
             | 
| 102 | 
            -
                      events.each do |event|
         | 
| 103 | 
            -
                        if error
         | 
| 104 | 
            -
                          event.merge!(mark_as_error(event, error))
         | 
| 105 | 
            -
                          handle_error_event(event, error)
         | 
| 106 | 
            -
                        else
         | 
| 107 | 
            -
                          handle_after_event(event)
         | 
| 108 | 
            -
                        end
         | 
| 52 | 
            +
                        prepare_event(event, &blk)
         | 
| 109 53 | 
             
                      end
         | 
| 110 54 | 
             
                    end
         | 
| 111 55 | 
             
                  end
         | 
| @@ -115,19 +59,82 @@ module Tobox | |
| 115 59 |  | 
| 116 60 | 
             
                private
         | 
| 117 61 |  | 
| 62 | 
            +
                def prepare_event(event)
         | 
| 63 | 
            +
                  event[:metadata] = try_json_parse(event[:metadata])
         | 
| 64 | 
            +
                  handle_before_event(event)
         | 
| 65 | 
            +
                  yield(to_message(event))
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def fetch_event_id
         | 
| 69 | 
            +
                  @pick_next_sql.for_update
         | 
| 70 | 
            +
                                .skip_locked
         | 
| 71 | 
            +
                                .limit(1).select_map(:id).first # lock starts here
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def mark_as_fetched(event_id)
         | 
| 75 | 
            +
                  @ds.where(id: event_id).update(@mark_as_fetched_params)
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def events_tr(&block)
         | 
| 79 | 
            +
                  @db.transaction(savepoint: false, &block)
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def event_id_tr
         | 
| 83 | 
            +
                  yield
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def with_event(event_id, &blk)
         | 
| 87 | 
            +
                  event, error = yield_event(event_id, &blk)
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  if error
         | 
| 90 | 
            +
                    event.merge!(mark_as_error(event, error))
         | 
| 91 | 
            +
                    handle_error_event(event, error)
         | 
| 92 | 
            +
                  else
         | 
| 93 | 
            +
                    handle_after_event(event)
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def yield_event(event_id)
         | 
| 98 | 
            +
                  events_ds = @ds.where(id: event_id)
         | 
| 99 | 
            +
                  event = error = nil
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  begin
         | 
| 102 | 
            +
                    event = events_ds.first
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    yield event
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                    events_ds.delete
         | 
| 107 | 
            +
                  rescue StandardError => e
         | 
| 108 | 
            +
                    error = e
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  [event, error]
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 118 114 | 
             
                def log_message(msg)
         | 
| 119 115 | 
             
                  "(worker: #{@label}) -> #{msg}"
         | 
| 120 116 | 
             
                end
         | 
| 121 117 |  | 
| 122 118 | 
             
                def mark_as_error(event, error)
         | 
| 123 | 
            -
                   | 
| 124 | 
            -
                    attempts: Sequel[@table][:attempts] + 1,
         | 
| 119 | 
            +
                  update_params = {
         | 
| 125 120 | 
             
                    run_at: Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
         | 
| 126 | 
            -
                                            seconds: event[:attempts]  | 
| 121 | 
            +
                                            seconds: @exponential_retry_factor**(event[:attempts] - 1)),
         | 
| 127 122 | 
             
                    # run_at: Sequel.date_add(Sequel::CURRENT_TIMESTAMP,
         | 
| 128 123 | 
             
                    #                         seconds: Sequel.function(:POWER, Sequel[@table][:attempts] + 1,  4)),
         | 
| 129 124 | 
             
                    last_error: "#{error.message}\n#{error.backtrace.join("\n")}"
         | 
| 130 | 
            -
                   | 
| 125 | 
            +
                  }
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  set_event_retry_attempts(event, update_params)
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def set_event_retry_attempts(event, update_params)
         | 
| 131 | 
            +
                  ds = @ds.where(id: event[:id])
         | 
| 132 | 
            +
                  if @ds.supports_returning?(:update)
         | 
| 133 | 
            +
                    ds.returning.update(update_params).first
         | 
| 134 | 
            +
                  else
         | 
| 135 | 
            +
                    ds.update(update_params)
         | 
| 136 | 
            +
                    ds.first
         | 
| 137 | 
            +
                  end
         | 
| 131 138 | 
             
                end
         | 
| 132 139 |  | 
| 133 140 | 
             
                def to_message(event)
         | 
| @@ -148,16 +155,6 @@ module Tobox | |
| 148 155 | 
             
                  data
         | 
| 149 156 | 
             
                end
         | 
| 150 157 |  | 
| 151 | 
            -
                def try_insert_inbox(event)
         | 
| 152 | 
            -
                  return yield unless @inbox_table && @inbox_column
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                  ret = @db[@inbox_table].insert_conflict.insert(@inbox_column => event[@inbox_column])
         | 
| 155 | 
            -
             | 
| 156 | 
            -
                  return unless ret
         | 
| 157 | 
            -
             | 
| 158 | 
            -
                  yield
         | 
| 159 | 
            -
                end
         | 
| 160 | 
            -
             | 
| 161 158 | 
             
                def handle_before_event(event)
         | 
| 162 159 | 
             
                  @logger.debug do
         | 
| 163 160 | 
             
                    log_message("outbox event (type: \"#{event[:type]}\", attempts: #{event[:attempts]}) starting...")
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Tobox
         | 
| 4 | 
            +
              module Plugins
         | 
| 5 | 
            +
                module EventGrouping
         | 
| 6 | 
            +
                  def self.configure(conf)
         | 
| 7 | 
            +
                    conf.config[:group_column] = :group_id
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  module FetcherMethods
         | 
| 11 | 
            +
                    def initialize(_, configuration)
         | 
| 12 | 
            +
                      super
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      @group_column = configuration[:group_column]
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    private
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    def fetch_event_id
         | 
| 20 | 
            +
                      group = @pick_next_sql.for_update
         | 
| 21 | 
            +
                                            .skip_locked
         | 
| 22 | 
            +
                                            .limit(1)
         | 
| 23 | 
            +
                                            .select(@group_column)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      # get total from a group, to compare to the number of future locked rows.
         | 
| 26 | 
            +
                      total_from_group = @ds.where(@group_column => group).count
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      event_ids = @ds.where(@group_column => group)
         | 
| 29 | 
            +
                                     .order(Sequel.desc(:run_at, nulls: :first), :id)
         | 
| 30 | 
            +
                                     .for_update.skip_locked.select_map(:id)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      if event_ids.size != total_from_group
         | 
| 33 | 
            +
                        # this happens if concurrent workers locked different rows from the same group,
         | 
| 34 | 
            +
                        # or when new rows from a given group have been inserted after the lock has been
         | 
| 35 | 
            +
                        # acquired
         | 
| 36 | 
            +
                        event_ids = []
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      # lock all, process 1
         | 
| 40 | 
            +
                      event_ids.first
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                register_plugin :event_grouping, EventGrouping
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Tobox
         | 
| 4 | 
            +
              module Plugins
         | 
| 5 | 
            +
                module Inbox
         | 
| 6 | 
            +
                  def self.configure(conf)
         | 
| 7 | 
            +
                    conf.config[:inbox_table] = :inbox
         | 
| 8 | 
            +
                    conf.config[:inbox_column] = :unique_id
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  module FetcherMethods
         | 
| 12 | 
            +
                    def initialize(_, configuration)
         | 
| 13 | 
            +
                      super
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      inbox_table = configuration[:inbox_table]
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                      @inbox_ds = @db[inbox_table]
         | 
| 18 | 
            +
                      @inbox_column = configuration[:inbox_column]
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    private
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def prepare_event(event, &blk)
         | 
| 24 | 
            +
                      try_insert_inbox(event) { super }
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    def try_insert_inbox(event)
         | 
| 28 | 
            +
                      if @inbox_ds.respond_to?(:supports_insert_conflict?) && @inbox_ds.supports_insert_conflict?
         | 
| 29 | 
            +
                        ret = @inbox_ds.insert_conflict.insert(@inbox_column => event[@inbox_column])
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                        return event unless ret
         | 
| 32 | 
            +
                      else
         | 
| 33 | 
            +
                        begin
         | 
| 34 | 
            +
                          @inbox_ds.insert(@inbox_column => event[@inbox_column])
         | 
| 35 | 
            +
                        rescue Sequel::UniqueConstraintViolation
         | 
| 36 | 
            +
                          return event
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
                      end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                      yield
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
                register_plugin :inbox, Inbox
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Tobox
         | 
| 4 | 
            +
              module Plugins
         | 
| 5 | 
            +
                module Progress
         | 
| 6 | 
            +
                  def self.configure(conf)
         | 
| 7 | 
            +
                    conf.config[:visibility_timeout] = 30
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  module FetcherMethods
         | 
| 11 | 
            +
                    private
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def initialize(_, configuration)
         | 
| 14 | 
            +
                      super
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      @mark_as_fetched_params[:run_at] = Sequel.date_add(
         | 
| 17 | 
            +
                        Sequel::CURRENT_TIMESTAMP,
         | 
| 18 | 
            +
                        seconds: configuration[:visibility_timeout]
         | 
| 19 | 
            +
                      )
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def events_tr
         | 
| 23 | 
            +
                      yield
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def event_id_tr(&block)
         | 
| 27 | 
            +
                      @db.transaction(savepoint: false, &block)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
                register_plugin :progress, Progress
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
    
        data/lib/tobox/plugins/stats.rb
    CHANGED
    
    | @@ -25,6 +25,8 @@ module Tobox | |
| 25 25 |  | 
| 26 26 | 
             
                      config = @config
         | 
| 27 27 |  | 
| 28 | 
            +
                      plugins = config.plugins.map(&:name)
         | 
| 29 | 
            +
             | 
| 28 30 | 
             
                      interval = config.stats_interval_seconds
         | 
| 29 31 | 
             
                      @stats_handlers = Array(config.lifecycle_events[:stats])
         | 
| 30 32 |  | 
| @@ -34,14 +36,24 @@ module Tobox | |
| 34 36 |  | 
| 35 37 | 
             
                      @max_attempts = config[:max_attempts]
         | 
| 36 38 |  | 
| 39 | 
            +
                      @created_at_column = config[:created_at_column]
         | 
| 40 | 
            +
             | 
| 37 41 | 
             
                      @db = Sequel.connect(config.database.opts.merge(max_connections: 1))
         | 
| 42 | 
            +
                      @db.loggers = config.database.loggers
         | 
| 38 43 | 
             
                      Array(config.lifecycle_events[:database_connect]).each { |cb| cb.call(@db) }
         | 
| 39 44 |  | 
| 40 | 
            -
                       | 
| 41 | 
            -
                      @outbox_ds = @db[ | 
| 45 | 
            +
                      outbox_table = config[:table]
         | 
| 46 | 
            +
                      @outbox_ds = @db[outbox_table]
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                      if plugins.include?("Tobox::Plugins::Inbox")
         | 
| 49 | 
            +
                        inbox_table = config[:inbox_table]
         | 
| 50 | 
            +
                        @inbox_ds = @db[inbox_table]
         | 
| 51 | 
            +
                      end
         | 
| 42 52 |  | 
| 43 | 
            -
                       | 
| 44 | 
            -
             | 
| 53 | 
            +
                      if @created_at_column
         | 
| 54 | 
            +
                        # discard already handled events
         | 
| 55 | 
            +
                        @oldest_event_age_ds = @outbox_ds.where(last_error: nil, run_at: nil).order(Sequel.asc(:id))
         | 
| 56 | 
            +
                      end
         | 
| 45 57 |  | 
| 46 58 | 
             
                      logger = config.default_logger
         | 
| 47 59 |  | 
| @@ -104,6 +116,13 @@ module Tobox | |
| 104 116 | 
             
                      stats[:failed_count] ||= 0
         | 
| 105 117 |  | 
| 106 118 | 
             
                      stats[:inbox_count] = @inbox_ds.count if @inbox_ds
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                      if @oldest_event_age_ds
         | 
| 121 | 
            +
                        created_at = @oldest_event_age_ds.get(@created_at_column)
         | 
| 122 | 
            +
                        age = created_at ? (Time.now - created_at).to_i : 0
         | 
| 123 | 
            +
                        stats[:oldest_event_age_in_seconds] = age
         | 
| 124 | 
            +
                      end
         | 
| 125 | 
            +
             | 
| 107 126 | 
             
                      stats
         | 
| 108 127 | 
             
                    end
         | 
| 109 128 | 
             
                  end
         | 
| @@ -5,34 +5,74 @@ require "fiber_scheduler" | |
| 5 5 |  | 
| 6 6 | 
             
            module Tobox
         | 
| 7 7 | 
             
              class FiberPool < Pool
         | 
| 8 | 
            -
                def initialize( | 
| 8 | 
            +
                def initialize(_)
         | 
| 9 9 | 
             
                  Sequel.extension(:fiber_concurrency)
         | 
| 10 10 | 
             
                  super
         | 
| 11 | 
            +
                  @fibers = []
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  @fiber_mtx = Mutex.new
         | 
| 14 | 
            +
                  @fiber_cond = ConditionVariable.new
         | 
| 15 | 
            +
                  @fiber_thread = nil
         | 
| 11 16 | 
             
                end
         | 
| 12 17 |  | 
| 13 18 | 
             
                def start
         | 
| 14 19 | 
             
                  @fiber_thread = Thread.start do
         | 
| 15 20 | 
             
                    Thread.current.name = "tobox-fibers-thread"
         | 
| 16 21 |  | 
| 17 | 
            -
                     | 
| 18 | 
            -
                       | 
| 19 | 
            -
                         | 
| 22 | 
            +
                    begin
         | 
| 23 | 
            +
                      FiberScheduler do
         | 
| 24 | 
            +
                        @fiber_mtx.synchronize do
         | 
| 25 | 
            +
                          @workers.each do |worker|
         | 
| 26 | 
            +
                            @fibers << start_fiber_worker(worker)
         | 
| 27 | 
            +
                          end
         | 
| 28 | 
            +
                          @fiber_cond.signal
         | 
| 29 | 
            +
                        end
         | 
| 20 30 | 
             
                      end
         | 
| 31 | 
            +
                    rescue KillError
         | 
| 32 | 
            +
                      @fibers.each { |f| f.raise(KillError) }
         | 
| 21 33 | 
             
                    end
         | 
| 22 34 | 
             
                  end
         | 
| 35 | 
            +
                  @fiber_mtx.synchronize do
         | 
| 36 | 
            +
                    @fiber_cond.wait(@fiber_mtx)
         | 
| 37 | 
            +
                  end
         | 
| 23 38 | 
             
                end
         | 
| 24 39 |  | 
| 25 40 | 
             
                def stop
         | 
| 26 41 | 
             
                  shutdown_timeout = @configuration[:shutdown_timeout]
         | 
| 42 | 
            +
                  grace_shutdown_timeout = @configuration[:grace_shutdown_timeout]
         | 
| 27 43 |  | 
| 28 44 | 
             
                  super
         | 
| 29 45 |  | 
| 30 | 
            -
                   | 
| 31 | 
            -
             | 
| 32 | 
            -
                   | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 46 | 
            +
                  @fiber_thread.join(shutdown_timeout)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  return unless @fiber_thread.alive?
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  @fiber_thread.raise(KillError)
         | 
| 51 | 
            +
                  @fiber_thread.join(grace_shutdown_timeout)
         | 
| 52 | 
            +
                  @fiber_thread.kill
         | 
| 53 | 
            +
                  @fiber_thread.join(1)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                private
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def start_fiber_worker(worker)
         | 
| 59 | 
            +
                  Fiber.schedule do
         | 
| 60 | 
            +
                    do_work(worker)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    @fiber_mtx.synchronize do
         | 
| 63 | 
            +
                      @fibers.delete(Fiber.current)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                      if worker.finished? && @running
         | 
| 66 | 
            +
                        idx = @workers.index(worker)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                        raise Error, "worker not found" unless idx
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                        subst_worker = Worker.new(worker.label, @configuration)
         | 
| 71 | 
            +
                        @workers[idx] = subst_worker
         | 
| 72 | 
            +
                        subst_fiber = start_fiber_worker(subst_worker)
         | 
| 73 | 
            +
                        @fiber_mtx.synchronize { @fibers << subst_fiber }
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                    end
         | 
| 36 76 | 
             
                  end
         | 
| 37 77 | 
             
                end
         | 
| 38 78 | 
             
              end
         | 
| @@ -22,24 +22,35 @@ module Tobox | |
| 22 22 |  | 
| 23 23 | 
             
                def stop
         | 
| 24 24 | 
             
                  shutdown_timeout = @configuration[:shutdown_timeout]
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                  deadline = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
         | 
| 25 | 
            +
                  grace_shutdown_timeout = @configuration[:grace_shutdown_timeout]
         | 
| 27 26 |  | 
| 28 27 | 
             
                  super
         | 
| 29 28 | 
             
                  Thread.pass # let workers finish
         | 
| 30 29 |  | 
| 31 30 | 
             
                  # soft exit
         | 
| 32 | 
            -
                   | 
| 33 | 
            -
                     | 
| 31 | 
            +
                  join = lambda do |timeout|
         | 
| 32 | 
            +
                    start = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    loop do
         | 
| 35 | 
            +
                      terminating_th = @threads.synchronize { @threads.first }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      return unless terminating_th
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                      break if elapsed > timeout
         | 
| 34 42 |  | 
| 35 | 
            -
             | 
| 43 | 
            +
                      terminating_th.join(timeout - elapsed)
         | 
| 44 | 
            +
                    end
         | 
| 36 45 | 
             
                  end
         | 
| 37 46 |  | 
| 47 | 
            +
                  join.call(shutdown_timeout)
         | 
| 48 | 
            +
             | 
| 38 49 | 
             
                  # hard exit
         | 
| 39 | 
            -
                  @threads.each { |th| th.raise(KillError) }
         | 
| 40 | 
            -
                   | 
| 41 | 
            -
             | 
| 42 | 
            -
                   | 
| 50 | 
            +
                  @threads.synchronize { @threads.each { |th| th.raise(KillError) } }
         | 
| 51 | 
            +
                  join.call(grace_shutdown_timeout)
         | 
| 52 | 
            +
                  @threads.synchronize { @threads.each(&:kill) }
         | 
| 53 | 
            +
                  join.call(1)
         | 
| 43 54 | 
             
                end
         | 
| 44 55 |  | 
| 45 56 | 
             
                private
         | 
| @@ -56,13 +67,13 @@ module Tobox | |
| 56 67 | 
             
                      if worker.finished? && @running
         | 
| 57 68 | 
             
                        idx = @workers.index(worker)
         | 
| 58 69 |  | 
| 70 | 
            +
                        raise Error, "worker not found" unless idx
         | 
| 71 | 
            +
             | 
| 59 72 | 
             
                        subst_worker = Worker.new(worker.label, @configuration)
         | 
| 60 73 | 
             
                        @workers[idx] = subst_worker
         | 
| 61 74 | 
             
                        subst_thread = start_thread_worker(subst_worker)
         | 
| 62 75 | 
             
                        @threads << subst_thread
         | 
| 63 76 | 
             
                      end
         | 
| 64 | 
            -
                      # all workers went down abruply, we need to kill the process.
         | 
| 65 | 
            -
                      # @parent_thread.raise(Interrupt) if wk.finished? && @threads.empty? && @running
         | 
| 66 77 | 
             
                    end
         | 
| 67 78 | 
             
                  end
         | 
| 68 79 | 
             
                end
         | 
    
        data/lib/tobox/pool.rb
    CHANGED
    
    
    
        data/lib/tobox/version.rb
    CHANGED
    
    
    
        data/lib/tobox/worker.rb
    CHANGED
    
    | @@ -8,7 +8,7 @@ module Tobox | |
| 8 8 | 
             
                  @label = label
         | 
| 9 9 | 
             
                  @wait_for_events_delay = configuration[:wait_for_events_delay]
         | 
| 10 10 | 
             
                  @handlers = configuration.handlers || {}
         | 
| 11 | 
            -
                  @fetcher =  | 
| 11 | 
            +
                  @fetcher = configuration.fetcher_class.new(label, configuration)
         | 
| 12 12 | 
             
                  @finished = false
         | 
| 13 13 |  | 
| 14 14 | 
             
                  return unless (message_to_arguments = configuration.arguments_handler)
         | 
    
        data/lib/tobox.rb
    CHANGED
    
    | @@ -9,6 +9,8 @@ require "mutex_m" | |
| 9 9 | 
             
            module Tobox
         | 
| 10 10 | 
             
              class Error < StandardError; end
         | 
| 11 11 |  | 
| 12 | 
            +
              EMPTY = [].freeze
         | 
| 13 | 
            +
             | 
| 12 14 | 
             
              module Plugins
         | 
| 13 15 | 
             
                @plugins = {}
         | 
| 14 16 | 
             
                @plugins.extend(Mutex_m)
         | 
| @@ -34,8 +36,8 @@ module Tobox | |
| 34 36 | 
             
              end
         | 
| 35 37 | 
             
            end
         | 
| 36 38 |  | 
| 37 | 
            -
            require_relative "tobox/configuration"
         | 
| 38 39 | 
             
            require_relative "tobox/fetcher"
         | 
| 39 40 | 
             
            require_relative "tobox/worker"
         | 
| 40 41 | 
             
            require_relative "tobox/pool"
         | 
| 41 42 | 
             
            require_relative "tobox/application"
         | 
| 43 | 
            +
            require_relative "tobox/configuration"
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: tobox
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.5.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - HoneyryderChuck
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2024- | 
| 11 | 
            +
            date: 2024-09-26 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: sequel
         | 
| @@ -45,6 +45,9 @@ files: | |
| 45 45 | 
             
            - lib/tobox/plugins/datadog/configuration.rb
         | 
| 46 46 | 
             
            - lib/tobox/plugins/datadog/integration.rb
         | 
| 47 47 | 
             
            - lib/tobox/plugins/datadog/patcher.rb
         | 
| 48 | 
            +
            - lib/tobox/plugins/event_grouping.rb
         | 
| 49 | 
            +
            - lib/tobox/plugins/inbox.rb
         | 
| 50 | 
            +
            - lib/tobox/plugins/progress.rb
         | 
| 48 51 | 
             
            - lib/tobox/plugins/sentry.rb
         | 
| 49 52 | 
             
            - lib/tobox/plugins/stats.rb
         | 
| 50 53 | 
             
            - lib/tobox/plugins/zeitwerk.rb
         | 
| @@ -71,14 +74,14 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 71 74 | 
             
              requirements:
         | 
| 72 75 | 
             
              - - ">="
         | 
| 73 76 | 
             
                - !ruby/object:Gem::Version
         | 
| 74 | 
            -
                  version: 2. | 
| 77 | 
            +
                  version: 2.7.0
         | 
| 75 78 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 76 79 | 
             
              requirements:
         | 
| 77 80 | 
             
              - - ">="
         | 
| 78 81 | 
             
                - !ruby/object:Gem::Version
         | 
| 79 82 | 
             
                  version: '0'
         | 
| 80 83 | 
             
            requirements: []
         | 
| 81 | 
            -
            rubygems_version: 3. | 
| 84 | 
            +
            rubygems_version: 3.5.3
         | 
| 82 85 | 
             
            signing_key:
         | 
| 83 86 | 
             
            specification_version: 4
         | 
| 84 87 | 
             
            summary: Transactional outbox pattern implementation in ruby
         |