joblin 0.1.0
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 +7 -0
- data/README.md +1 -0
- data/app/models/joblin/background_task/api_access.rb +148 -0
- data/app/models/joblin/background_task/attachments.rb +47 -0
- data/app/models/joblin/background_task/executor.rb +63 -0
- data/app/models/joblin/background_task/options.rb +75 -0
- data/app/models/joblin/background_task/retention_policy.rb +28 -0
- data/app/models/joblin/background_task.rb +72 -0
- data/app/models/joblin/concerns/job_working_dirs.rb +21 -0
- data/db/migrate/20250903184852_create_background_tasks.rb +12 -0
- data/joblin.gemspec +35 -0
- data/lib/joblin/batching/batch.rb +537 -0
- data/lib/joblin/batching/callback.rb +135 -0
- data/lib/joblin/batching/chain_builder.rb +247 -0
- data/lib/joblin/batching/compat/active_job.rb +108 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/css/styles.less +182 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/batch_tree.js +108 -0
- data/lib/joblin/batching/compat/sidekiq/web/batches_assets/js/util.js +2 -0
- data/lib/joblin/batching/compat/sidekiq/web/helpers.rb +41 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_batch_tree.erb +6 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +44 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_common.erb +13 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +21 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/_pagination.erb +26 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +81 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/batches.erb +23 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +137 -0
- data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +47 -0
- data/lib/joblin/batching/compat/sidekiq/web.rb +218 -0
- data/lib/joblin/batching/compat/sidekiq.rb +149 -0
- data/lib/joblin/batching/compat.rb +20 -0
- data/lib/joblin/batching/context_hash.rb +157 -0
- data/lib/joblin/batching/hier_batch_ids.lua +25 -0
- data/lib/joblin/batching/jobs/base_job.rb +7 -0
- data/lib/joblin/batching/jobs/concurrent_batch_job.rb +20 -0
- data/lib/joblin/batching/jobs/managed_batch_job.rb +175 -0
- data/lib/joblin/batching/jobs/serial_batch_job.rb +20 -0
- data/lib/joblin/batching/pool.rb +254 -0
- data/lib/joblin/batching/pool_refill.lua +47 -0
- data/lib/joblin/batching/schedule_callback.lua +14 -0
- data/lib/joblin/batching/status.rb +89 -0
- data/lib/joblin/engine.rb +15 -0
- data/lib/joblin/lazy_access.rb +72 -0
- data/lib/joblin/uniqueness/compat/active_job.rb +75 -0
- data/lib/joblin/uniqueness/compat/sidekiq.rb +135 -0
- data/lib/joblin/uniqueness/compat.rb +20 -0
- data/lib/joblin/uniqueness/configuration.rb +25 -0
- data/lib/joblin/uniqueness/job_uniqueness.rb +49 -0
- data/lib/joblin/uniqueness/lock_context.rb +199 -0
- data/lib/joblin/uniqueness/locksmith.rb +92 -0
- data/lib/joblin/uniqueness/on_conflict/base.rb +32 -0
- data/lib/joblin/uniqueness/on_conflict/log.rb +13 -0
- data/lib/joblin/uniqueness/on_conflict/null_strategy.rb +9 -0
- data/lib/joblin/uniqueness/on_conflict/raise.rb +11 -0
- data/lib/joblin/uniqueness/on_conflict/reject.rb +21 -0
- data/lib/joblin/uniqueness/on_conflict/reschedule.rb +20 -0
- data/lib/joblin/uniqueness/on_conflict.rb +62 -0
- data/lib/joblin/uniqueness/strategy/base.rb +107 -0
- data/lib/joblin/uniqueness/strategy/until_and_while_executing.rb +35 -0
- data/lib/joblin/uniqueness/strategy/until_executed.rb +20 -0
- data/lib/joblin/uniqueness/strategy/until_executing.rb +20 -0
- data/lib/joblin/uniqueness/strategy/until_expired.rb +16 -0
- data/lib/joblin/uniqueness/strategy/while_executing.rb +26 -0
- data/lib/joblin/uniqueness/strategy.rb +27 -0
- data/lib/joblin/uniqueness/unique_job_common.rb +79 -0
- data/lib/joblin/version.rb +3 -0
- data/lib/joblin.rb +37 -0
- data/spec/batching/batch_spec.rb +493 -0
- data/spec/batching/callback_spec.rb +38 -0
- data/spec/batching/compat/active_job_spec.rb +107 -0
- data/spec/batching/compat/sidekiq_spec.rb +127 -0
- data/spec/batching/context_hash_spec.rb +54 -0
- data/spec/batching/flow_spec.rb +82 -0
- data/spec/batching/integration/fail_then_succeed.rb +42 -0
- data/spec/batching/integration/integration.rb +57 -0
- data/spec/batching/integration/nested.rb +88 -0
- data/spec/batching/integration/simple.rb +47 -0
- data/spec/batching/integration/workflow.rb +134 -0
- data/spec/batching/integration_helper.rb +50 -0
- data/spec/batching/pool_spec.rb +161 -0
- data/spec/batching/status_spec.rb +76 -0
- data/spec/batching/support/base_job.rb +19 -0
- data/spec/batching/support/sample_callback.rb +2 -0
- data/spec/internal/config/database.yml +5 -0
- data/spec/internal/config/routes.rb +5 -0
- data/spec/internal/config/storage.yml +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +6 -0
- data/spec/internal/log/test.log +48200 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/models/background_task_spec.rb +41 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/uniqueness/compat/active_job_spec.rb +49 -0
- data/spec/uniqueness/compat/sidekiq_spec.rb +68 -0
- data/spec/uniqueness/lock_context_spec.rb +106 -0
- data/spec/uniqueness/on_conflict/log_spec.rb +11 -0
- data/spec/uniqueness/on_conflict/raise_spec.rb +10 -0
- data/spec/uniqueness/on_conflict/reschedule_spec.rb +63 -0
- data/spec/uniqueness/on_conflict_spec.rb +16 -0
- data/spec/uniqueness/spec_helper.rb +19 -0
- data/spec/uniqueness/strategy/base_spec.rb +100 -0
- data/spec/uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
- data/spec/uniqueness/strategy/until_executed_spec.rb +23 -0
- data/spec/uniqueness/strategy/until_executing_spec.rb +23 -0
- data/spec/uniqueness/strategy/until_expired_spec.rb +23 -0
- data/spec/uniqueness/strategy/while_executing_spec.rb +33 -0
- data/spec/uniqueness/support/lock_strategy.rb +28 -0
- data/spec/uniqueness/support/on_conflict.rb +24 -0
- data/spec/uniqueness/support/test_worker.rb +19 -0
- data/spec/uniqueness/unique_job_common_spec.rb +45 -0
- metadata +308 -0
| @@ -0,0 +1,127 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            RSpec.describe Joblin::Batching::Compat::Sidekiq do
         | 
| 4 | 
            +
              describe Joblin::Batching::Compat::Sidekiq::ServerMiddleware do
         | 
| 5 | 
            +
                context 'when without batch' do
         | 
| 6 | 
            +
                  it 'just yields' do
         | 
| 7 | 
            +
                    yielded = false
         | 
| 8 | 
            +
                    expect(Joblin::Batching::Batch).not_to receive(:process_successful_job)
         | 
| 9 | 
            +
                    expect(Joblin::Batching::Batch).not_to receive(:process_failed_job)
         | 
| 10 | 
            +
                    subject.call(nil, {}, nil) { yielded = true }
         | 
| 11 | 
            +
                    expect(yielded).to be_truthy
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                context 'when in batch' do
         | 
| 16 | 
            +
                  let(:bid) { 'SAMPLEBID' }
         | 
| 17 | 
            +
                  let(:jid) { 'SAMPLEJID' }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  context 'when successful' do
         | 
| 20 | 
            +
                    it 'yields' do
         | 
| 21 | 
            +
                      yielded = false
         | 
| 22 | 
            +
                      subject.call(nil, { 'bid' => bid, 'jid' => jid }, nil) { yielded = true }
         | 
| 23 | 
            +
                      expect(yielded).to be_truthy
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    it 'calls process_successful_job' do
         | 
| 27 | 
            +
                      expect(Joblin::Batching::Batch).to receive(:process_successful_job).with(bid, nil)
         | 
| 28 | 
            +
                      subject.call(nil, { 'bid' => bid }, nil) {}
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  context 'when failed' do
         | 
| 33 | 
            +
                    it 'calls process_failed_job and reraises exception' do
         | 
| 34 | 
            +
                      reraised = false
         | 
| 35 | 
            +
                      expect(Joblin::Batching::Batch).to receive(:process_failed_job)
         | 
| 36 | 
            +
                      begin
         | 
| 37 | 
            +
                        subject.call(nil, { 'bid' => bid }, nil) { raise 'ERR' }
         | 
| 38 | 
            +
                      rescue
         | 
| 39 | 
            +
                        reraised = true
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
                      expect(reraised).to be_truthy
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              describe Joblin::Batching::Compat::Sidekiq::ClientMiddleware do
         | 
| 48 | 
            +
                context 'when without batch' do
         | 
| 49 | 
            +
                  it 'just yields' do
         | 
| 50 | 
            +
                    yielded = false
         | 
| 51 | 
            +
                    expect(Joblin::Batching::Batch).not_to receive(:append_jobs)
         | 
| 52 | 
            +
                    subject.call(nil, {}, nil) { yielded = true }
         | 
| 53 | 
            +
                    expect(yielded).to be_truthy
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                context 'when in batch' do
         | 
| 58 | 
            +
                  let(:bid) { 'SAMPLEBID' }
         | 
| 59 | 
            +
                  let(:jid) { 'SAMPLEJID' }
         | 
| 60 | 
            +
                  before { Thread.current[Joblin::Batching::CURRENT_BATCH_THREAD_KEY] = Joblin::Batching::Batch.new(bid) }
         | 
| 61 | 
            +
                  after { Thread.current[Joblin::Batching::CURRENT_BATCH_THREAD_KEY] = nil }
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  it 'yields' do
         | 
| 64 | 
            +
                    yielded = false
         | 
| 65 | 
            +
                    subject.call(nil, { 'jid' => jid }, nil) { yielded = true }
         | 
| 66 | 
            +
                    expect(yielded).to be_truthy
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  it 'increments job queue' do
         | 
| 70 | 
            +
                    # expect(Joblin::Batching::Batch).to receive(:append_jobs).with(bid)
         | 
| 71 | 
            +
                    # subject.call(nil, { 'jid' => jid }, nil) {}
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  it 'assigns bid to msg' do
         | 
| 75 | 
            +
                    msg = { 'jid' => jid }
         | 
| 76 | 
            +
                    subject.call(nil, msg, nil) {}
         | 
| 77 | 
            +
                    expect(msg['bid']).to eq(bid)
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
            RSpec.describe Joblin::Batching::Compat::Sidekiq do
         | 
| 84 | 
            +
              let(:config) { defined?(Sidekiq::Config) ? double(Sidekiq::Config) : class_double(Sidekiq) }
         | 
| 85 | 
            +
              let(:client_middleware) { double(Sidekiq::Middleware::Chain) }
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              context 'client' do
         | 
| 88 | 
            +
                it 'adds client middleware' do
         | 
| 89 | 
            +
                  allow(Sidekiq).to receive(:configure_client).and_yield(config)
         | 
| 90 | 
            +
                  expect(config).to receive(:client_middleware).and_yield(client_middleware)
         | 
| 91 | 
            +
                  expect(client_middleware).to receive(:add).with(Joblin::Batching::Compat::Sidekiq::ClientMiddleware)
         | 
| 92 | 
            +
                  Joblin::Batching::Compat::Sidekiq.instance_variable_set(:@already_configured, false)
         | 
| 93 | 
            +
                  Joblin::Batching::Compat::Sidekiq.configure
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
              context 'server' do
         | 
| 98 | 
            +
                let(:server_middleware) { double(Sidekiq::Middleware::Chain) }
         | 
| 99 | 
            +
                let(:death_handlers) { double(Array) }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                it 'adds client and server middleware' do
         | 
| 102 | 
            +
                  allow(Sidekiq).to receive(:configure_server).and_yield(config)
         | 
| 103 | 
            +
                  expect(config).to receive(:client_middleware).and_yield(client_middleware)
         | 
| 104 | 
            +
                  expect(config).to receive(:server_middleware).and_yield(server_middleware)
         | 
| 105 | 
            +
                  expect(config).to receive(:death_handlers).and_return(death_handlers)
         | 
| 106 | 
            +
                  expect(client_middleware).to receive(:add).with(Joblin::Batching::Compat::Sidekiq::ClientMiddleware)
         | 
| 107 | 
            +
                  expect(server_middleware).to receive(:add).with(Joblin::Batching::Compat::Sidekiq::ServerMiddleware)
         | 
| 108 | 
            +
                  expect(death_handlers).to receive(:<<)
         | 
| 109 | 
            +
                  Joblin::Batching::Compat::Sidekiq.instance_variable_set(:@already_configured, false)
         | 
| 110 | 
            +
                  Joblin::Batching::Compat::Sidekiq.configure
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
              context 'worker' do
         | 
| 115 | 
            +
                it 'defines method bid' do
         | 
| 116 | 
            +
                  expect(Sidekiq::Worker.instance_methods).to include(:bid)
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                it 'defines method batch' do
         | 
| 120 | 
            +
                  expect(Sidekiq::Worker.instance_methods).to include(:batch)
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                it 'defines method valid_within_batch?' do
         | 
| 124 | 
            +
                  expect(Sidekiq::Worker.instance_methods).to include(:valid_within_batch?)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
              end
         | 
| 127 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            RSpec.describe Joblin::Batching::ContextHash do
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              class ContextedJob < BatchTestJobBase
         | 
| 6 | 
            +
                def perform
         | 
| 7 | 
            +
                  was_performed(batch_context.flatten)
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def was_performed(*args); end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def self.callback_perform(*args)
         | 
| 13 | 
            +
                  perform_later
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              it "works with first-level jobs" do
         | 
| 18 | 
            +
                ActiveJob::Base.queue_adapter = :sidekiq
         | 
| 19 | 
            +
                b = Joblin::Batching::Batch.new
         | 
| 20 | 
            +
                b.context = { hello: 'world' }
         | 
| 21 | 
            +
                expect_any_instance_of(ContextedJob).to receive(:was_performed).with({ 'hello' => 'world' })
         | 
| 22 | 
            +
                b.jobs do
         | 
| 23 | 
            +
                  ContextedJob.perform_later
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
                Sidekiq::Worker.drain_all
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              it "works with nested-batch jobs" do
         | 
| 29 | 
            +
                ActiveJob::Base.queue_adapter = :sidekiq
         | 
| 30 | 
            +
                b = Joblin::Batching::Batch.new
         | 
| 31 | 
            +
                b.context = { hello: 'world', foo: 'bar' }
         | 
| 32 | 
            +
                expect_any_instance_of(ContextedJob).to receive(:was_performed).with({ 'hello' => 'world', 'foo' => 'baz', 'some' => 'other' })
         | 
| 33 | 
            +
                b.jobs do
         | 
| 34 | 
            +
                  b2 = Joblin::Batching::Batch.new
         | 
| 35 | 
            +
                  b2.context = { some: 'other', foo: 'baz' }
         | 
| 36 | 
            +
                  b2.jobs do
         | 
| 37 | 
            +
                    ContextedJob.perform_later
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
                Sidekiq::Worker.drain_all
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              it 'works with a callback batch' do
         | 
| 44 | 
            +
                ActiveJob::Base.queue_adapter = :sidekiq
         | 
| 45 | 
            +
                b = Joblin::Batching::Batch.new
         | 
| 46 | 
            +
                b.context = { hello: 'world' }
         | 
| 47 | 
            +
                b.on(:success, "ContextedJob.callback_perform")
         | 
| 48 | 
            +
                expect_any_instance_of(ContextedJob).to receive(:was_performed).with({ 'hello' => 'world' })
         | 
| 49 | 
            +
                b.jobs do
         | 
| 50 | 
            +
                  BatchTestJobBase.perform_later
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
                Sidekiq::Worker.drain_all
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class WorkerA < BatchTestJobBase
         | 
| 4 | 
            +
              def perform
         | 
| 5 | 
            +
              end
         | 
| 6 | 
            +
            end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            class WorkerB < BatchTestJobBase
         | 
| 9 | 
            +
              def perform
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
            end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            class WorkerC < BatchTestJobBase
         | 
| 14 | 
            +
              def perform
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            RSpec.describe 'Batch flow' do
         | 
| 19 | 
            +
              context 'when handling a batch' do
         | 
| 20 | 
            +
                let(:batch) { Joblin::Batching::Batch.new }
         | 
| 21 | 
            +
                before { batch.on(:complete, SampleCallback, :id => 42) }
         | 
| 22 | 
            +
                before { batch.description = 'describing the batch' }
         | 
| 23 | 
            +
                let(:status) { Joblin::Batching::Batch::Status.new(batch.bid) }
         | 
| 24 | 
            +
                let(:queue) { Sidekiq::Queue.new }
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                it 'correctly initializes' do
         | 
| 27 | 
            +
                  expect(batch.bid).not_to be_nil
         | 
| 28 | 
            +
                  expect(batch.description).to eq('describing the batch')
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  batch.jobs do
         | 
| 31 | 
            +
                    3.times do
         | 
| 32 | 
            +
                      BatchTestWorker.perform_async
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  expect(status.pending).to eq(3)
         | 
| 37 | 
            +
                  expect(status.failures).to eq(0)
         | 
| 38 | 
            +
                  expect(status.complete?).to be false
         | 
| 39 | 
            +
                  expect(status.created_at).not_to be_nil
         | 
| 40 | 
            +
                  expect(status.bid).to eq(batch.bid)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              context 'when handling a nested batch' do
         | 
| 45 | 
            +
                let(:batchA) { Joblin::Batching::Batch.new }
         | 
| 46 | 
            +
                let(:batchB) { Joblin::Batching::Batch.new }
         | 
| 47 | 
            +
                let(:batchC) { Joblin::Batching::Batch.new(batchA.bid) }
         | 
| 48 | 
            +
                let(:batchD) { Joblin::Batching::Batch.new }
         | 
| 49 | 
            +
                let(:jids) { [] }
         | 
| 50 | 
            +
                let(:parent) { batchA.bid }
         | 
| 51 | 
            +
                let(:children) { [] }
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                it 'handles a basic nested batch' do
         | 
| 54 | 
            +
                  batchA.jobs do
         | 
| 55 | 
            +
                    jids << WorkerA.perform_async
         | 
| 56 | 
            +
                    batchB.jobs do
         | 
| 57 | 
            +
                      jids << WorkerB.perform_async
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
                    jids << WorkerA.perform_async
         | 
| 60 | 
            +
                    children << batchB.bid
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  batchC.jobs do
         | 
| 64 | 
            +
                    batchD.jobs do
         | 
| 65 | 
            +
                      jids << WorkerC.perform_async
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                    children << batchD.bid
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  expect(jids.size).to eq(4)
         | 
| 71 | 
            +
                  expect(Joblin::Batching::Batch::Status.new(parent).child_count).to eq(2)
         | 
| 72 | 
            +
                  children.each do |kid|
         | 
| 73 | 
            +
                      status = Joblin::Batching::Batch::Status.new(kid)
         | 
| 74 | 
            +
                      expect(status.child_count).to eq(0)
         | 
| 75 | 
            +
                      expect(status.pending).to eq(1)
         | 
| 76 | 
            +
                      expect(status.parent_bid).to eq(parent)
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require_relative '../integration_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Workflow when a Job fails, retries, and then succeeds
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class Worker1
         | 
| 6 | 
            +
              include Sidekiq::Worker
         | 
| 7 | 
            +
              sidekiq_options retry: 5
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              @@failed = false
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def perform
         | 
| 12 | 
            +
                Sidekiq.logger.info "Work 1"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                unless @@failed
         | 
| 15 | 
            +
                  @@failed = true
         | 
| 16 | 
            +
                  raise "One Failure"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            class MyCallback
         | 
| 22 | 
            +
              def on_success(status, options)
         | 
| 23 | 
            +
                Sidekiq.logger.info "Overall Success #{options} #{status.data}"
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
              alias_method :multi, :on_success
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def on_complete(status, options)
         | 
| 28 | 
            +
                Sidekiq.logger.info "Overall Complete #{options} #{status.data}"
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            overall = Joblin::Batching::Batch.new
         | 
| 33 | 
            +
            overall.on(:success, MyCallback, to: 'success@gmail.com')
         | 
| 34 | 
            +
            overall.on(:complete, MyCallback, to: 'success@gmail.com')
         | 
| 35 | 
            +
            overall.jobs do
         | 
| 36 | 
            +
              Worker1.perform_async
         | 
| 37 | 
            +
            end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            puts "Overall bid #{overall.bid}"
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            output, keys = process_tests
         | 
| 42 | 
            +
            overall_tests(output, keys, file: __FILE__)
         | 
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            require 'integration_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Simple test of adding jobs to the current batch
         | 
| 4 | 
            +
            # Batches:
         | 
| 5 | 
            +
            # - Overall (TestWoker) + Another worker
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class AnotherWorker
         | 
| 8 | 
            +
              include Sidekiq::Worker
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def perform
         | 
| 11 | 
            +
                Sidekiq.logger.info "Another Worker"
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class TestWorker
         | 
| 16 | 
            +
              include Sidekiq::Worker
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def perform
         | 
| 19 | 
            +
                Sidekiq.logger.info "Test Worker"
         | 
| 20 | 
            +
                if bid
         | 
| 21 | 
            +
                  batch.jobs do
         | 
| 22 | 
            +
                    AnotherWorker.perform_async
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            class MyCallback
         | 
| 29 | 
            +
              def on_success(status, options)
         | 
| 30 | 
            +
                Sidekiq.logger.info "Success #{options} #{status.data}"
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
              alias_method :multi, :on_success
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def on_complete(status, options)
         | 
| 35 | 
            +
                Sidekiq.logger.info "Complete #{options} #{status.data}"
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            batch = Joblin::Batching::Batch.new
         | 
| 40 | 
            +
            batch.description = 'Test batch'
         | 
| 41 | 
            +
            batch.callback_queue = :default
         | 
| 42 | 
            +
            batch.on(:success, 'MyCallback#on_success', to: 'success@gmail.com')
         | 
| 43 | 
            +
            batch.on(:success, 'MyCallback#multi', to: 'success@gmail.com')
         | 
| 44 | 
            +
            batch.on(:complete, MyCallback, to: 'complete@gmail.com')
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            batch.jobs do
         | 
| 47 | 
            +
              10.times do
         | 
| 48 | 
            +
                TestWorker.perform_async
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         | 
| 51 | 
            +
            puts Joblin::Batching::Batch::Status.new(batch.bid).data
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            dump_redis_keys
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            Sidekiq::Worker.drain_all
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            dump_redis_keys
         | 
| @@ -0,0 +1,88 @@ | |
| 1 | 
            +
            require 'integration_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Tests deep nesting of batches
         | 
| 4 | 
            +
            # Batches:
         | 
| 5 | 
            +
            # - Overall (Worker 1)
         | 
| 6 | 
            +
            #  - Worker 2
         | 
| 7 | 
            +
            #   - Worker 3
         | 
| 8 | 
            +
            #    - Worker 4
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class Worker1
         | 
| 11 | 
            +
              include Sidekiq::Worker
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def perform
         | 
| 14 | 
            +
                Sidekiq.logger.info "Work1"
         | 
| 15 | 
            +
                batch = Joblin::Batching::Batch.new
         | 
| 16 | 
            +
                batch.on(:success, Worker2)
         | 
| 17 | 
            +
                batch.jobs do
         | 
| 18 | 
            +
                  Worker2.perform_async
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            class Worker2
         | 
| 24 | 
            +
              include Sidekiq::Worker
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              def perform
         | 
| 27 | 
            +
                Sidekiq.logger.info "Work2"
         | 
| 28 | 
            +
                batch = Joblin::Batching::Batch.new
         | 
| 29 | 
            +
                batch.on(:success, Worker3)
         | 
| 30 | 
            +
                batch.jobs do
         | 
| 31 | 
            +
                  Worker3.perform_async
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def on_success status, opts
         | 
| 36 | 
            +
                Sidekiq.logger.info "Worker 2 Success"
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            class Worker3
         | 
| 41 | 
            +
              include Sidekiq::Worker
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def perform
         | 
| 44 | 
            +
                Sidekiq.logger.info "Work3"
         | 
| 45 | 
            +
                batch = Joblin::Batching::Batch.new
         | 
| 46 | 
            +
                batch.on(:success, Worker4)
         | 
| 47 | 
            +
                batch.jobs do
         | 
| 48 | 
            +
                  Worker4.perform_async
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def on_success status, opts
         | 
| 53 | 
            +
                Sidekiq.logger.info "Worker 3 Success"
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            class Worker4
         | 
| 58 | 
            +
              include Sidekiq::Worker
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def perform
         | 
| 61 | 
            +
                Sidekiq.logger.info "Work4"
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def on_success status, opts
         | 
| 65 | 
            +
                Sidekiq.logger.info "Worker 4 Success"
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
             | 
| 70 | 
            +
            class SomeClass
         | 
| 71 | 
            +
              def on_complete(status, options)
         | 
| 72 | 
            +
                Sidekiq.logger.info "Overall Complete #{options} #{status.data}"
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
              def on_success(status, options)
         | 
| 75 | 
            +
                Sidekiq.logger.info "Overall Success #{options} #{status.data}"
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
            end
         | 
| 78 | 
            +
            batch = Joblin::Batching::Batch.new
         | 
| 79 | 
            +
            batch.on(:success, SomeClass, 'uid' => 3)
         | 
| 80 | 
            +
            batch.on(:complete, SomeClass, 'uid' => 3)
         | 
| 81 | 
            +
            batch.jobs do
         | 
| 82 | 
            +
              Worker1.perform_async
         | 
| 83 | 
            +
            end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
            puts "Overall bid #{batch.bid}"
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            output, keys = process_tests
         | 
| 88 | 
            +
            overall_tests output, keys
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            require 'integration_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Simple nested batch without callbacks
         | 
| 4 | 
            +
            # Batches:
         | 
| 5 | 
            +
            # - Overall (Worker1)
         | 
| 6 | 
            +
            #  - Worker2
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            class Worker1
         | 
| 9 | 
            +
              include Sidekiq::Worker
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def perform
         | 
| 12 | 
            +
                Sidekiq.logger.info "Work1"
         | 
| 13 | 
            +
                batch = Joblin::Batching::Batch.new
         | 
| 14 | 
            +
                batch.jobs do
         | 
| 15 | 
            +
                  Worker2.perform_async
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            class Worker2
         | 
| 21 | 
            +
              include Sidekiq::Worker
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def perform
         | 
| 24 | 
            +
                Sidekiq.logger.info "Work2"
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            class SomeClass
         | 
| 29 | 
            +
              def on_complete(status, options)
         | 
| 30 | 
            +
                Sidekiq.logger.info "Overall Complete #{options} #{status.data}"
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
              def on_success(status, options)
         | 
| 33 | 
            +
                Sidekiq.logger.info "Overall Success #{options} #{status.data}"
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            batch = Joblin::Batching::Batch.new
         | 
| 38 | 
            +
            batch.on(:success, SomeClass)
         | 
| 39 | 
            +
            batch.on(:complete, SomeClass)
         | 
| 40 | 
            +
            batch.jobs do
         | 
| 41 | 
            +
              Worker1.perform_async
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            puts "Overall bid #{batch.bid}"
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            output, keys = process_tests
         | 
| 47 | 
            +
            overall_tests output, keys
         | 
| @@ -0,0 +1,134 @@ | |
| 1 | 
            +
            require 'integration_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Complex workflow with sequential and nested
         | 
| 4 | 
            +
            # Also test sub batches without callbacks
         | 
| 5 | 
            +
            # Batches:
         | 
| 6 | 
            +
            # - Overall
         | 
| 7 | 
            +
            #  - Worker1
         | 
| 8 | 
            +
            #   - Worker3
         | 
| 9 | 
            +
            #  - Worker2 + Worker3
         | 
| 10 | 
            +
            #   - Worker1
         | 
| 11 | 
            +
            #    - Worker3
         | 
| 12 | 
            +
            #  - Worker4
         | 
| 13 | 
            +
            #  - Worker5
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Callbacks
         | 
| 16 | 
            +
              def worker1 status, opts
         | 
| 17 | 
            +
                Sidekiq.logger.info "Success 1 #{status.data}"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                overall = Joblin::Batching::Batch.new status.parent_bid
         | 
| 20 | 
            +
                overall.jobs do
         | 
| 21 | 
            +
                  batch = Joblin::Batching::Batch.new
         | 
| 22 | 
            +
                  batch.on(:success, "Callbacks#worker2")
         | 
| 23 | 
            +
                  batch.jobs do
         | 
| 24 | 
            +
                    Worker2.perform_async
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def worker2 status, opts
         | 
| 30 | 
            +
                Sidekiq.logger.info "Success 2 #{status.data}"
         | 
| 31 | 
            +
                overall = Joblin::Batching::Batch.new status.parent_bid
         | 
| 32 | 
            +
                overall.jobs do
         | 
| 33 | 
            +
                  batch = Joblin::Batching::Batch.new
         | 
| 34 | 
            +
                  batch.on(:success, "Callbacks#worker4")
         | 
| 35 | 
            +
                  batch.jobs do
         | 
| 36 | 
            +
                    Worker4.perform_async
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              def worker4 status, opts
         | 
| 43 | 
            +
                Sidekiq.logger.info "Success 4 #{status.data}"
         | 
| 44 | 
            +
                overall = Joblin::Batching::Batch.new status.parent_bid
         | 
| 45 | 
            +
                overall.jobs do
         | 
| 46 | 
            +
                  batch = Joblin::Batching::Batch.new
         | 
| 47 | 
            +
                  batch.on(:success, "Callbacks#worker5")
         | 
| 48 | 
            +
                  batch.jobs do
         | 
| 49 | 
            +
                    Worker5.perform_async
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              def worker5 status, opts
         | 
| 55 | 
            +
                Sidekiq.logger.info "Success 5 #{status.data}"
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            class Worker1
         | 
| 60 | 
            +
              include Sidekiq::Worker
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              def perform
         | 
| 63 | 
            +
                Sidekiq.logger.info "Work 1"
         | 
| 64 | 
            +
                batch = Joblin::Batching::Batch.new
         | 
| 65 | 
            +
                batch.jobs do
         | 
| 66 | 
            +
                  Worker3.perform_async
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
            end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            class Worker2
         | 
| 72 | 
            +
              include Sidekiq::Worker
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              def perform
         | 
| 75 | 
            +
                Sidekiq.logger.info "Work 2"
         | 
| 76 | 
            +
                if bid
         | 
| 77 | 
            +
                  batch.jobs do
         | 
| 78 | 
            +
                    Worker3.perform_async
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                  newb = Joblin::Batching::Batch.new
         | 
| 81 | 
            +
                  newb.jobs do
         | 
| 82 | 
            +
                    Worker1.perform_async
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            class Worker3
         | 
| 89 | 
            +
              include Sidekiq::Worker
         | 
| 90 | 
            +
              def perform
         | 
| 91 | 
            +
                Sidekiq.logger.info "Work 3"
         | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
            end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            class Worker4
         | 
| 96 | 
            +
              include Sidekiq::Worker
         | 
| 97 | 
            +
              def perform
         | 
| 98 | 
            +
                Sidekiq.logger.info "Work 4"
         | 
| 99 | 
            +
              end
         | 
| 100 | 
            +
            end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
            class Worker5
         | 
| 103 | 
            +
              include Sidekiq::Worker
         | 
| 104 | 
            +
              def perform
         | 
| 105 | 
            +
                Sidekiq.logger.info "Work 5"
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
            end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            class MyCallback
         | 
| 110 | 
            +
              def on_success(status, options)
         | 
| 111 | 
            +
                Sidekiq.logger.info "Overall Success #{options} #{status.data}"
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
              alias_method :multi, :on_success
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              def on_complete(status, options)
         | 
| 116 | 
            +
                Sidekiq.logger.info "Overall Complete #{options} #{status.data}"
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
            end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
            overall = Joblin::Batching::Batch.new
         | 
| 121 | 
            +
            overall.on(:success, MyCallback, to: 'success@gmail.com')
         | 
| 122 | 
            +
            overall.on(:complete, MyCallback, to: 'success@gmail.com')
         | 
| 123 | 
            +
            overall.jobs do
         | 
| 124 | 
            +
              batch1 = Joblin::Batching::Batch.new
         | 
| 125 | 
            +
              batch1.on(:success, "Callbacks#worker1")
         | 
| 126 | 
            +
              batch1.jobs do
         | 
| 127 | 
            +
                Worker1.perform_async
         | 
| 128 | 
            +
              end
         | 
| 129 | 
            +
            end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
            puts "Overall bid #{overall.bid}"
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            output, keys = process_tests
         | 
| 134 | 
            +
            overall_tests output, keys
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'sidekiq/testing'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            Sidekiq::Testing.server_middleware do |chain|
         | 
| 5 | 
            +
              chain.add Joblin::Batching::Compat::Sidekiq::ServerMiddleware
         | 
| 6 | 
            +
            end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Joblin.redis { |r| r.flushdb }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            def redis_keys
         | 
| 11 | 
            +
              Joblin.redis { |r| r.keys('BID-*') }
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            def dump_redis_keys
         | 
| 15 | 
            +
              puts redis_keys.inspect
         | 
| 16 | 
            +
            end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            def process_tests
         | 
| 19 | 
            +
              out_buf = StringIO.new
         | 
| 20 | 
            +
              Sidekiq.logger = Logger.new out_buf
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              # Sidekiq.logger.level = :info
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              Sidekiq::Worker.drain_all
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              output = out_buf.string
         | 
| 27 | 
            +
              keys = redis_keys
         | 
| 28 | 
            +
              puts out_buf.string
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              [output, keys]
         | 
| 31 | 
            +
            end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            def overall_tests(output, keys, file: nil)
         | 
| 34 | 
            +
              test_name = "Batch Integration Test"
         | 
| 35 | 
            +
              test_name = File.basename(file, ".*")  if file
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              Rspec.describe test_name do
         | 
| 38 | 
            +
                it "runs overall complete callback" do
         | 
| 39 | 
            +
                  expect(output).to include "Overall Complete"
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                it "runs overall success callback" do
         | 
| 43 | 
            +
                  expect(output).to include "Overall Success"
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                it "cleans redis keys" do
         | 
| 47 | 
            +
                  expect(keys).to eq([])
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         |