Full Code of cookpad/barbeque for AI

master 0bd975c84da6 cached
253 files
2.5 MB
660.6k tokens
1455 symbols
1 requests
Download .txt
Showing preview only (2,642K chars total). Download the full file or copy to clipboard to get everything.
Repository: cookpad/barbeque
Branch: master
Commit: 0bd975c84da6
Files: 253
Total size: 2.5 MB

Directory structure:
gitextract_3xnxz7z0/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rspec
├── CHANGELOG.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── app/
│   ├── assets/
│   │   ├── config/
│   │   │   └── barbeque_manifest.js
│   │   ├── images/
│   │   │   └── barbeque/
│   │   │       └── .keep
│   │   ├── javascripts/
│   │   │   └── barbeque/
│   │   │       ├── application.js
│   │   │       ├── job_definitions.js
│   │   │       └── job_queues.js
│   │   └── stylesheets/
│   │       └── barbeque/
│   │           ├── application.css
│   │           └── job_definitions.css
│   ├── controllers/
│   │   └── barbeque/
│   │       ├── api/
│   │       │   ├── application_controller.rb
│   │       │   ├── job_executions_controller.rb
│   │       │   ├── job_retries_controller.rb
│   │       │   └── revision_locks_controller.rb
│   │       ├── application_controller.rb
│   │       ├── apps_controller.rb
│   │       ├── job_definitions_controller.rb
│   │       ├── job_executions_controller.rb
│   │       ├── job_queues_controller.rb
│   │       ├── job_retries_controller.rb
│   │       ├── monitors_controller.rb
│   │       └── sns_subscriptions_controller.rb
│   ├── helpers/
│   │   └── barbeque/
│   │       ├── application_helper.rb
│   │       ├── job_definitions_helper.rb
│   │       └── job_executions_helper.rb
│   ├── models/
│   │   ├── barbeque/
│   │   │   ├── api/
│   │   │   │   ├── application_resource.rb
│   │   │   │   ├── database_maintenance_resource.rb
│   │   │   │   ├── job_execution_resource.rb
│   │   │   │   ├── job_retry_resource.rb
│   │   │   │   └── revision_lock_resource.rb
│   │   │   ├── app.rb
│   │   │   ├── application_record.rb
│   │   │   ├── docker_container.rb
│   │   │   ├── ecs_hako_task.rb
│   │   │   ├── job_definition.rb
│   │   │   ├── job_execution.rb
│   │   │   ├── job_queue.rb
│   │   │   ├── job_retry.rb
│   │   │   ├── retry_config.rb
│   │   │   ├── slack_notification.rb
│   │   │   └── sns_subscription.rb
│   │   └── concerns/
│   │       └── .keep
│   ├── services/
│   │   └── barbeque/
│   │       ├── message_enqueuing_service.rb
│   │       ├── message_retrying_service.rb
│   │       └── sns_subscription_service.rb
│   └── views/
│       ├── barbeque/
│       │   ├── apps/
│       │   │   ├── _form.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   └── show.html.haml
│       │   ├── job_definitions/
│       │   │   ├── _form.html.haml
│       │   │   ├── _retry_configuration_field.html.haml
│       │   │   ├── _slack_notification_field.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   ├── show.html.haml
│       │   │   └── stats.html.haml
│       │   ├── job_executions/
│       │   │   └── show.html.haml
│       │   ├── job_queues/
│       │   │   ├── _form.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   └── show.html.haml
│       │   ├── job_retries/
│       │   │   └── show.html.haml
│       │   ├── monitors/
│       │   │   └── index.html.haml
│       │   └── sns_subscriptions/
│       │       ├── _form.html.haml
│       │       ├── edit.html.haml
│       │       ├── index.html.haml
│       │       ├── new.html.haml
│       │       └── show.html.haml
│       └── layouts/
│           └── barbeque/
│               ├── _header.html.haml
│               ├── _sidebar.html.haml
│               ├── application.html.haml
│               ├── apps.html.haml
│               ├── job_definitions.html.haml
│               ├── job_executions.html.haml
│               ├── job_queues.html.haml
│               ├── job_retries.html.haml
│               ├── monitors.html.haml
│               └── sns_subscriptions.html.haml
├── barbeque.gemspec
├── bin/
│   └── rails
├── compose.yaml
├── config/
│   ├── initializers/
│   │   └── garage.rb
│   └── routes.rb
├── db/
│   └── migrate/
│       ├── 20160217020910_create_job_queues.rb
│       ├── 20160219010912_create_job_executions.rb
│       ├── 20160223060807_create_apps.rb
│       ├── 20160223183348_create_job_definitions.rb
│       ├── 20160225020801_add_job_definition_id_to_job_executions.rb
│       ├── 20160412083604_add_finished_at_to_job_executions.rb
│       ├── 20160415043427_create_slack_notifications.rb
│       ├── 20160509041452_add_job_queue_id_to_job_executions.rb
│       ├── 20160516041710_create_job_retries.rb
│       ├── 20160829023237_prefix_barbeque_to_tables.rb
│       ├── 20170420030157_create_barbeque_sns_subscriptions.rb
│       ├── 20170711085157_create_barbeque_docker_containers.rb
│       ├── 20170712075449_create_barbeque_ecs_hako_tasks.rb
│       ├── 20170724025542_add_index_to_job_execution_status.rb
│       ├── 20180411070937_add_index_to_barbeque_job_executions_created_at.rb
│       ├── 20190221050714_create_barbeque_retry_configs.rb
│       ├── 20190311034445_add_notify_failure_only_if_retry_limit_reached_to_barbeque_slack_notifications.rb
│       ├── 20190315052951_change_barbeque_retry_configs_base_delay_default_value.rb
│       ├── 20191029105530_change_job_name_to_case_sensitive.rb
│       └── 20240415080757_fix_collations.rb
├── doc/
│   ├── api/
│   │   ├── job_executions.md
│   │   ├── job_retries.md
│   │   └── revision_locks.md
│   └── toc.md
├── lib/
│   ├── barbeque/
│   │   ├── config.rb
│   │   ├── docker_image.rb
│   │   ├── engine.rb
│   │   ├── exception_handler.rb
│   │   ├── execution_log.rb
│   │   ├── execution_poller.rb
│   │   ├── executor/
│   │   │   ├── docker.rb
│   │   │   └── hako.rb
│   │   ├── executor.rb
│   │   ├── hako_s3_client.rb
│   │   ├── maintenance.rb
│   │   ├── message/
│   │   │   ├── base.rb
│   │   │   ├── invalid_message.rb
│   │   │   ├── job_execution.rb
│   │   │   ├── job_retry.rb
│   │   │   └── notification.rb
│   │   ├── message.rb
│   │   ├── message_handler/
│   │   │   ├── job_execution.rb
│   │   │   ├── job_retry.rb
│   │   │   └── notification.rb
│   │   ├── message_handler.rb
│   │   ├── message_queue.rb
│   │   ├── retry_poller.rb
│   │   ├── runner.rb
│   │   ├── slack_client.rb
│   │   ├── slack_notifier.rb
│   │   ├── version.rb
│   │   └── worker.rb
│   ├── barbeque.rb
│   └── tasks/
│       └── barbeque_tasks.rake
├── spec/
│   ├── barbeque/
│   │   ├── config_builder_spec.rb
│   │   ├── config_spec.rb
│   │   ├── docker_image_spec.rb
│   │   ├── exception_handler_spec.rb
│   │   ├── execution_log_spec.rb
│   │   ├── execution_poller_spec.rb
│   │   ├── executor/
│   │   │   ├── docker_spec.rb
│   │   │   └── hako_spec.rb
│   │   ├── executor_spec.rb
│   │   ├── message_handler/
│   │   │   ├── job_execution_spec.rb
│   │   │   ├── job_retry_spec.rb
│   │   │   └── notification_spec.rb
│   │   ├── message_queue_spec.rb
│   │   ├── message_spec.rb
│   │   ├── retry_poller_spec.rb
│   │   ├── runner_spec.rb
│   │   └── worker_spec.rb
│   ├── controllers/
│   │   └── barbeque/
│   │       ├── apps_controller_spec.rb
│   │       ├── job_definitions_controller_spec.rb
│   │       ├── job_executions_controller_spec.rb
│   │       ├── job_queues_controller_spec.rb
│   │       ├── job_retries_controller_spec.rb
│   │       └── sns_subscriptions_controller_spec.rb
│   ├── dummy/
│   │   ├── .gitignore
│   │   ├── Rakefile
│   │   ├── app/
│   │   │   ├── assets/
│   │   │   │   ├── config/
│   │   │   │   │   └── manifest.js
│   │   │   │   ├── images/
│   │   │   │   │   └── .keep
│   │   │   │   ├── javascripts/
│   │   │   │   │   ├── application.js
│   │   │   │   │   └── channels/
│   │   │   │   │       └── .keep
│   │   │   │   └── stylesheets/
│   │   │   │       └── application.css
│   │   │   ├── controllers/
│   │   │   │   ├── application_controller.rb
│   │   │   │   └── concerns/
│   │   │   │       └── .keep
│   │   │   ├── helpers/
│   │   │   │   └── application_helper.rb
│   │   │   ├── models/
│   │   │   │   └── concerns/
│   │   │   │       └── .keep
│   │   │   └── views/
│   │   │       └── layouts/
│   │   │           ├── application.html.erb
│   │   │           ├── mailer.html.erb
│   │   │           └── mailer.text.erb
│   │   ├── bin/
│   │   │   ├── bundle
│   │   │   ├── rails
│   │   │   ├── rake
│   │   │   ├── setup
│   │   │   ├── spring
│   │   │   ├── update
│   │   │   └── yarn
│   │   ├── config/
│   │   │   ├── application.rb
│   │   │   ├── barbeque.empty.yml
│   │   │   ├── barbeque.erb.yml
│   │   │   ├── barbeque.hako.yml
│   │   │   ├── barbeque.yml
│   │   │   ├── boot.rb
│   │   │   ├── database.yml
│   │   │   ├── environment.rb
│   │   │   ├── environments/
│   │   │   │   ├── development.rb
│   │   │   │   ├── production.rb
│   │   │   │   └── test.rb
│   │   │   ├── initializers/
│   │   │   │   ├── application_controller_renderer.rb
│   │   │   │   ├── assets.rb
│   │   │   │   ├── backtrace_silencers.rb
│   │   │   │   ├── content_security_policy.rb
│   │   │   │   ├── cookies_serializer.rb
│   │   │   │   ├── filter_parameter_logging.rb
│   │   │   │   ├── inflections.rb
│   │   │   │   ├── mime_types.rb
│   │   │   │   ├── permissions_policy.rb
│   │   │   │   ├── session_store.rb
│   │   │   │   └── wrap_parameters.rb
│   │   │   ├── locales/
│   │   │   │   └── en.yml
│   │   │   ├── puma.rb
│   │   │   ├── routes.rb
│   │   │   ├── secrets.yml
│   │   │   ├── spring.rb
│   │   │   └── storage.yml
│   │   ├── config.ru
│   │   ├── db/
│   │   │   ├── schema.rb
│   │   │   └── seeds.rb
│   │   ├── lib/
│   │   │   ├── assets/
│   │   │   │   └── .keep
│   │   │   └── tasks/
│   │   │       └── .keep
│   │   ├── log/
│   │   │   └── .keep
│   │   ├── public/
│   │   │   ├── 404.html
│   │   │   ├── 422.html
│   │   │   ├── 500.html
│   │   │   └── robots.txt
│   │   ├── tmp/
│   │   │   └── .keep
│   │   └── vendor/
│   │       └── assets/
│   │           ├── javascripts/
│   │           │   └── .keep
│   │           └── stylesheets/
│   │               └── .keep
│   ├── factories/
│   │   ├── app.rb
│   │   ├── job_definition.rb
│   │   ├── job_execution.rb
│   │   ├── job_queue.rb
│   │   ├── job_retry.rb
│   │   ├── retry_config.rb
│   │   ├── slack_notifications.rb
│   │   └── sns_subscription.rb
│   ├── models/
│   │   └── barbeque/
│   │       ├── job_definition_spec.rb
│   │       └── retry_config_spec.rb
│   ├── rails_helper.rb
│   ├── requests/
│   │   └── api/
│   │       ├── job_executions_spec.rb
│   │       ├── job_retries_spec.rb
│   │       └── revision_locks_spec.rb
│   ├── services/
│   │   └── message_enqueuing_service_spec.rb
│   └── spec_helper.rb
├── tools/
│   └── s3-log-migrator.rb
└── vendor/
    └── assets/
        ├── javascripts/
        │   ├── adminlte.js
        │   ├── bootstrap.js
        │   └── plotly-basic.js
        └── stylesheets/
            ├── AdminLTE.css
            ├── bootstrap.css
            └── skins/
                └── skin-blue.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        ruby:
          - '3.0'
          - '3.1'
          - '3.2'
          - '3.3'
    name: Run test with Ruby ${{ matrix.ruby }}
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: barbeque_root
          MYSQL_USER: barbeque
          MYSQL_PASSWORD: barbeque
          MYSQL_DATABASE: barbeque
        ports:
          - 3306:3306
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      RAILS_ENV: test
      DATABASE_URL: mysql2://barbeque:barbeque@127.0.0.1:3306/barbeque
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
          bundler-cache: true
      - run: bin/rails db:setup
      - run: bin/rails zeitwerk:check
      - run: bundle exec rspec


================================================
FILE: .gitignore
================================================
.bundle/
log/*.log
pkg/
Gemfile.lock


================================================
FILE: .rspec
================================================
--color
--require spec_helper


================================================
FILE: CHANGELOG.md
================================================
## v2.10.0 (2025-07-22)
### Changes
- Update Rails to 7.0
- Use `Bundler.with_unbundled_env` instead of deprecated `Bundler.with_clean_env`

## v2.9.0 (2025-01-24)
### Changes
- Update Rails to 6.1
  - For Zeitwerk, Barbeque::SNSSubscription and Barbeque::SNSSubscriptionService are renamed to Barbeque::SnsSubscription and Barbeque::SnsSubscriptionService respectively
- Support Ruby 3.0
  - Drop support for Ruby < 3.0
- Drop support for MySQL 5.7
  - A new migration is added to fix collations properly in MySQL 8.0

## v2.8.0 (2021-12-23)
### New features
- Pass `BARBEQUE_SENT_TIMESTAMP` variable to invoked jobs
  - The value is epoch time in milliseconds when the message is sent to the queue. See also: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html

## v2.7.5 (2020-05-29)
### Bug fixes
- Use kaminari helper to generate links safely

## v2.7.4 (2020-04-08)
### Bug fixes
- Delete the same message multiple times when DeleteMessage results in partial deletion of copies

## v2.7.3 (2020-01-08)
### Bug fixes
- Accept retry configuration on create

## v2.7.2 (2019-11-05)
### Bug fixes
- Wrap JSON message in pre tag for large message

### Changes
- Change job name to case-sensitive

## v2.7.1 (2019-09-13)
### Bug fixes
- Do not count pending retried job executions when checking `maximum_concurrent_executions`

## v2.7.0 (2019-03-18)
### New features
- Add "Notify failure event to Slack only if retry limit reached" option

### Changes
- Change the default value of "Base delay" option from 0.3 seconds to 15 seconds

## v2.6.0 (2019-02-25)
### New features
- Add server-side retry feature

### Changes
- Stop deleting job executions when job definition is deleted
  - Job executions tend to have large number of records, so deleting them is impossible.
- Return 503 in maintenance mode when mysql2 error occurs
- Use `BARBEQUE_HOST` environment variable to generate `html_url` field in API response

## v2.5.0 (2018-08-24)
### New features
- Add selectable `message` field to `GET /v1/job_executions/:message_id` response
- Add `BARBEQUE_VERIFY_ENQUEUED_JOBS` flag to API server which enables the feature that verifies the enqueued job by accessing MySQL
- Add `delay_seconds` parameter to support SQS's delay_seconds
  - This also supports ActiveJob's enqueue_at method.

### Bug fixes
- Show all SNS topics in /sns_subscriptions/new

## v2.4.0 (2018-04-13)
### Changes
- Update Rails to 5.2

## v2.3.0 (2018-04-12)
### Changes
- Add index to barbeque_job_executions.created_at
  - Be careful when you have large number of records in barbeque_job_executions table.

## v2.2.0 (2018-03-07)
### Changes
- Limit concurrent executions per job queue
  - `maximum_concurrent_executions` was applied to all job executions regardless of job queues.
  - Now `maximum_concurrent_executions` is applied to each job queue.
- Poll job executions and job retries only of the specified queue
  - All execution pollers ware polling all job execution/retry statuses.
  - Now execution pollers poll execution/retry statuses of their own job queue.

## v2.1.0 (2017-12-22)
### Improvements
- Support Hako definitions written in Jsonnet
  - Jsonnet format is supported since Hako v2.0.0
- Rename yaml_dir to definition_dir in config/barbeque.yml
  - yaml_dir is still supported with warnings for now

## v2.0.1 (2017-10-04)
### Improvements
- Build queue_url without database when maintenance mode is enabled
  - See https://github.com/cookpad/barbeque/pull/58 for detail

## v2.0.0 (2017-09-19)
### Incompatibilities
- Job execution URL was changed from `/job_executions/:id` to `/job_executions/:message_id`
  - Barbeque v1.0 links are redirected to v2.0 links
  - Job retry URL `/job_executions/:id/job_retries/:id` is also redirected to `/job_executions/:message_id/job_retries/:id`

## v1.4.1 (2017-09-05)
### Bug fixes
- Do not create execution record when sqs:DeleteMessage returns error

## v1.4.0 (2017-08-31)
### Improvements
- Update aws-sdk to v3
  - Use modularized aws-sdk gems

## v1.3.1 (2017-08-31)
### Improvements
- Filter job executions by status

## v1.3.0 (2017-08-21)
### New features
- Show SQS metrics in job queue page

### Improvements
- Update plotly.js to v1.29.3

### Bug fixes
- Do not truncate hover labels in /monitors chart
- Fix Slack notification field in job definition form

## v1.2.2 (2017-08-04)
### Improvements
- Extract S3 client for hako tasks

## v1.2.1 (2017-08-03)
### Bug fixes
- Do not create job_execution record when S3 returns error
- Ignore S3 errors when starting an execution

### Improvements
- Set descriptive title element
- Add breadcrumbs to all pages

## v1.2.0 (2017-07-26)
### Changes
- Update Rails to 5.1

## v1.1.0 (2017-07-25)
### Changes
- Add message context to exception handler
  - Now exception handler is able to track which message is being processed when an exception is raised
### Bug fixes
- Set status to running after creating related records

## v1.0.0 (2017-07-24)
- Introduce Executor as a replacement of Runner
  - `runner` and `runner_options` is renamed to `executor` and `executor_options` respectively
  - Now `rake barbeque:worker` launches three types of process
    - Runner: receives message from SQS queue, starts job execution and stores its identifier to the database
      - In Executor::Docker, the identifier is container id
      - In Executor::Hako, the identifier is ECS cluster and task ARN
    - ExecutionPoller: polls execution status and reflect it to the database
      - In Executor::Docker, uses `docker inspect` command
      - In Executor::Docker, uses S3 task notification JSON
    - RetryPoller: polls retry status and reflect it to the database
      - Same with ExecutionPoller
  - Add `maximum_concurrent_executions` configuration to config/barbeque.yml
    - It controls the number of concurrent job executions
    - The limit is disabled by default
- Drop support for legacy S3 log format
  - Run [migration script](tools/s3-log-migrator.rb) before upgrading to v1.0.0
- Add `sqs_receive_message_wait_time` configuration to config/barbeque.yml
  - This option controls ReceiveMessageWaitTimeSeconds attribute of SQS queue
  - The default value is changed from 20s to 10s

## v0.7.0 (2017-07-12)
- Change S3 log format [#29](https://github.com/cookpad/barbeque/pull/29)
  - The legacy format saves `{message: message.body.to_json, stdout: stdout, stderr: stderr}.to_json` to `#{app}/#{job}/#{message_id}`
  - The new format saves message body to `#{app}/#{job}/#{message_id}/message.json`, stdout to `#{app}/#{job}/#{message_id}/stdout.txt`, and stderr to `#{app}/#{job}/#{message_id}/stderr.txt`
  - The legacy format is still supported in v0.7.0, but will be removed in v1.0.0
    - Migration script is available: [tools/s3-log-migrator.rb](tools/s3-log-migrator.rb)

## v0.6.3 (2017-07-10)
- Add "running" status [#28](https://github.com/cookpad/barbeque/pull/28)

## v0.6.2 (2017-06-06)
- Kill N+1 query [#27](https://github.com/cookpad/barbeque/pull/27)

## v0.6.1 (2017-06-06)
- Show application names for each job definition in SNS subscriptions [#26](https://github.com/cookpad/barbeque/pull/26)

## v0.6.0 (2017-06-05)
- Support JSON-formatted string as Notification massages [#25](https://github.com/cookpad/barbeque/pull/25)

## v0.5.2 (2017-05-23)
- Destroy SNS subscriptions before destroying job definition [#24](https://github.com/cookpad/barbeque/pull/24)

## v0.5.1 (2017-05-01)
- Log message body in error status for retry [#23](https://github.com/cookpad/barbeque/pull/23)

## v0.5.0 (2017-05-01)
- Add error status to job_execution [#22](https://github.com/cookpad/barbeque/pull/22)

## v0.4.1 (2017-04-28)
- Add error handling for AWS SNS API calls [#21](https://github.com/cookpad/barbeque/pull/21)

## v0.4.0 (2017-04-27)
- Support fan-out executions using AWS SNS notifications [#20](https://github.com/cookpad/barbeque/pull/20)

## v0.3.0 (2017-04-17)
- Fix job_retry order in job_execution page [#16](https://github.com/cookpad/barbeque/pull/16)
- Fix Back path to each job definition page [#17](https://github.com/cookpad/barbeque/pull/17)
- Fix "active" class in sidebar [#18](https://github.com/cookpad/barbeque/pull/18)
- Add new page to show recently processed jobs [#19](https://github.com/cookpad/barbeque/pull/19)

## v0.2.4 (2017-04-05)
- Autolink URLs in job_retry outputs [#15](https://github.com/cookpad/barbeque/pull/15)

## v0.2.3 (2017-03-24)
- Make operation to deduplicate messages atomic [#14](https://github.com/cookpad/barbeque/pull/14)

## v0.2.2 (2017-03-16)
- Add execution id and html_url to status API response [#13](https://github.com/cookpad/barbeque/pull/13)

## v0.2.1
- Fix bug in execution statistics [#12](https://github.com/cookpad/barbeque/pull/12)

## v0.2.0
- Add Hako runner [#11](https://github.com/cookpad/barbeque/pull/11)

## v0.1.0
- Handle S3 error on web console [#10](https://github.com/cookpad/barbeque/pull/10)

## v0.0.18
- Reuse AWS credentials assumed from Role [#9](https://github.com/cookpad/barbeque/pull/9)

## v0.0.17
- Move statistics button to upper right on job definition page
- Link app from job definitions index

## v0.0.16
- Autolink stdout and stderr [#8](https://github.com/cookpad/barbeque/pull/8)

## v0.0.15
- Report exception raised in SQS message parser

## v0.0.14
- Allow logging worker exception by Raven [#7](https://github.com/cookpad/barbeque/pull/7)

## v0.0.13
- Allow switching log output by `BARBEQUE_LOG_TO_STDOUT` [#6](https://github.com/cookpad/barbeque/pull/4)

## v0.0.12
- Destroy job definitions after their app destruction [#4](https://github.com/cookpad/barbeque/pull/4)


================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'

# Declare your gem's dependencies in barbeque.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec

# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.

group :development, :test do
  gem 'pry-byebug'
end

# Following gems don't work if they're required on spec_helper.
# It should be loaded on `Bundler.require` or `before_configuration`,
# and I don't want to to load them on `before_configuration` for test environment.
group :test do
  gem 'rails-controller-testing'
end

# Workaround for https://github.com/rails/rails/pull/54264
gem 'concurrent-ruby', '< 1.3.5'


================================================
FILE: MIT-LICENSE
================================================
Copyright 2016 Takashi Kokubun

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# Barbeque [![Build Status](https://github.com/cookpad/barbeque/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/cookpad/barbeque/actions/workflows/ci.yml)

Job queue system to run job with Docker

<img src="https://raw.githubusercontent.com/cookpad/barbeque/master/doc/images/job_definitions.png" height="280px" />
<img src="https://raw.githubusercontent.com/cookpad/barbeque/master/doc/images/statistics.png" height="280px" />

## Project Status

Barbeque is used on production at Cookpad.

## What's Barbeque?

Barbeque is a job queue system that consists of:

- Web console to manage jobs
- Web API to queue a job
- Worker to execute a job

A job for Barbeque is a command you configured on web console.
A message serialized by JSON and a job name are given to the command when performed.
In Barbeque worker, they are done on Docker container.

## Why Barbeque?

- You can achieve job-level auto scaling using tools like [Amazon ECS](https://aws.amazon.com/ecs/) [EC2 Auto Scaling group](https://aws.amazon.com/autoscaling/)
  - For Amazon ECS, Barbeque has Hako executor
- You don't have to manage infrastructure for each application like Resque or Sidekiq

For details, see [Scalable Job Queue System Built with Docker // Speaker Deck](https://speakerdeck.com/k0kubun/scalable-job-queue-system-built-with-docker).

## Deployment

### Web API & console

Install barbeque.gem to an empty Rails app and mount `Barbeque::Engine`.
And deploy it as you like.

You also need to prepare MySQL, Amazon SQS and Amazon S3.

#### For sandbox environment
Barbeque's enqueue API tries to be independent of MySQL by design.
Although that design policy, verifying the enqueued job is useful in some environment (such as sandboxed environment).
Passing `BARBEQUE_VERIFY_ENQUEUED_JOBS=1` to the Web API server enables the feature that verifies the enqueued job by accessing MySQL.

### Worker

```bash
$ rake barbeque:worker BARBEQUE_QUEUE=default
```

The rake task launches four worker processes.

- Two runners
  - receives message from SQS queue, starts job execution and stores its identifier to the database
- One execution poller
  - gets execution status and reflect it to the database
- One retry poller
  - gets retried execution status and reflect it to the database

## Usage

Web API documentation is available at [doc/toc.md](./doc/toc.md).

### Ruby

[barbeque\_client.gem](https://github.com/cookpad/barbeque_client) has API client and ActiveJob integration.

## Executor
Barbeque executor can be customized in config/barbeque.yml. Executor is responsible for starting executions and getting status of executions.

Barbeque has currently two executors.

### Docker (default)
Barbeque::Executor::Docker starts execution by `docker run --detach` and gets status by `docker inspect`.

### Hako
Barbeque::Executor::Hako starts execution by `hako oneshot --no-wait` and gets status from S3 task notification.

#### Requirement
You must configure CloudWatch Events for putting S3 task notification.
See Hako's documentation for detail.
https://github.com/eagletmt/hako/blob/master/docs/ecs-task-notification.md

## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).


================================================
FILE: Rakefile
================================================
begin
  require 'bundler/setup'
rescue LoadError
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end

require 'rdoc/task'

RDoc::Task.new(:rdoc) do |rdoc|
  rdoc.rdoc_dir = 'rdoc'
  rdoc.title    = 'Barbeque'
  rdoc.options << '--line-numbers'
  rdoc.rdoc_files.include('README.md')
  rdoc.rdoc_files.include('lib/**/*.rb')
end

require 'bundler/gem_tasks'
require File.expand_path('../spec/dummy/config/application', __FILE__)

namespace :plotly do
  desc 'Update plotly.js to specified version'
  task :update, [:version] do |t, args|
    sh "curl -sSfL https://github.com/plotly/plotly.js/archive/v#{args[:version]}.tar.gz | tar zxf - plotly.js-#{args[:version]}/dist/plotly-basic.js -O > vendor/assets/javascripts/plotly-basic.js"
  end
end

namespace :bootstrap do
  desc 'Update Bootstrap to specified version'
  task :update, [:version] do |t, args|
    version = args.fetch(:version)
    zipfile = "bootstrap-v#{version}.zip"
    sh "curl -sSfL -o #{zipfile} https://github.com/twbs/bootstrap/releases/download/v#{version}/bootstrap-#{version}-dist.zip"
    sh "bsdtar xf #{zipfile} --strip-components 2 -C vendor/assets/stylesheets bootstrap-#{version}-dist/css/bootstrap.css"
    sh "bsdtar xf #{zipfile} --strip-components 2 -C vendor/assets/javascripts bootstrap-#{version}-dist/js/bootstrap.js"
  end
end

namespace :adminlte do
  desc 'Update AdminLTE to specified version'
  task :update, [:version] do |t, args|
    version = args.fetch(:version)
    tarball = "adminlte-v#{version}.tar.gz"
    sh "curl -sSfL -o #{tarball} https://github.com/ColorlibHQ/AdminLTE/archive/refs/tags/v#{version}.tar.gz"
    sh "tar zxf #{tarball} --strip-components 3 -C vendor/assets/stylesheets AdminLTE-#{version}/dist/css/AdminLTE.css AdminLTE-#{version}/dist/css/skins/skin-blue.css"
    sh "tar zxf #{tarball} --strip-components 3 -C vendor/assets/javascripts AdminLTE-#{version}/dist/js/adminlte.js"
  end
end

Rails.application.load_tasks


================================================
FILE: app/assets/config/barbeque_manifest.js
================================================
//= link_directory ../javascripts/barbeque .js
//= link_directory ../stylesheets/barbeque .css


================================================
FILE: app/assets/images/barbeque/.keep
================================================


================================================
FILE: app/assets/javascripts/barbeque/application.js
================================================
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require bootstrap
//= require jquery_ujs
//= require adminlte
//= require plotly-basic
//= require_tree .


================================================
FILE: app/assets/javascripts/barbeque/job_definitions.js
================================================
jQuery(function($) {
  if (!document.querySelector('.barbeque_job_definitions_controller')) {
    return;
  }

  $('.use_slack_notification').bind('change', function(event) {
    const enabledField = $('.slack_notification_field');
    if (event.target.value === 'true') {
      enabledField.removeClass('active');
    } else {
      enabledField.addClass('active');
    }
  });

  $('.enable_retry_configuration').bind('change', function(event) {
    const enabledField = $('.retry_configuration_field');
    if (event.target.value === 'true') {
      enabledField.removeClass('active');
    } else {
      enabledField.addClass('active');
    }
  });
});


================================================
FILE: app/assets/javascripts/barbeque/job_queues.js
================================================
jQuery(function($) {
  if (!document.querySelector('.barbeque_job_queues_controller')) {
    return;
  }
  const sqsDiv = document.getElementById('sqs-attributes');
  if (!sqsDiv) {
    return;
  }

  const { url, metricsUrl } = sqsDiv.dataset;
  $.getJSON(url).done(data => {
    renderBox(sqsDiv, 'SQS queue metrics', metricsUrl, data);
    if (data.dlq) {
      const dlqDiv = document.getElementById('sqs-dlq-attributes');
      renderBox(dlqDiv, 'SQS dead-letter queue metrics', metricsUrl, data.dlq);
    }
  }).fail(jqxhr => {
    const errorMessage = document.createElement('div');
    errorMessage.classList.add('alert');
    errorMessage.classList.add('alert-danger');
    errorMessage.appendChild(document.createTextNode(`Server returned ${jqxhr.status}: ${jqxhr.statusText}`));

    const indicator = sqsDiv.querySelector('.loading-indicator');
    if (indicator) {
      indicator.parentNode.removeChild(indicator);
    }
    sqsDiv.appendChild(errorMessage);
  });
});

const renderBox = (div, title, metricsUrl, data) => {
  const box = document.createElement('div');
  box.classList.add('box');
  const boxHeader = document.createElement('div');
  boxHeader.classList.add('box-header');
  const boxTitle = document.createElement('h3');
  boxTitle.classList.add('box-title');
  boxTitle.classList.add('with_padding');
  boxTitle.appendChild(document.createTextNode(title));
  boxHeader.appendChild(boxTitle);
  const boxBody = document.createElement('div');
  boxBody.classList.add('box-body');
  box.appendChild(boxHeader);
  box.appendChild(boxBody);

  const table = document.createElement('table');
  table.classList.add('table');
  table.classList.add('table-bordered');
  const thead = document.createElement('thead');
  const theadTr = document.createElement('tr');
  thead.appendChild(theadTr);
  const tbody = document.createElement('tbody');
  const tbodyTr = document.createElement('tr');
  tbody.appendChild(tbodyTr);
  for (const [name, value] of Object.entries(data.attributes)) {
    const th = document.createElement('th');
    th.appendChild(document.createTextNode(name));
    theadTr.appendChild(th);
    const td = document.createElement('td');
    td.appendChild(document.createTextNode(value));
    tbodyTr.appendChild(td);
  }
  table.appendChild(thead);
  table.appendChild(tbody);
  boxBody.appendChild(table);

  const indicator = div.querySelector('.loading-indicator');
  if (indicator) {
    indicator.parentNode.removeChild(indicator);
  }
  div.appendChild(box);

  const metrics = {
    NumberOfMessagesSent: 'Sum',
    ApproximateNumberOfMessagesVisible: 'Sum',
    ApproximateNumberOfMessagesNotVisible: 'Sum',
    ApproximateAgeOfOldestMessage: 'Maximum',
  };
  const row = document.createElement('div');
  row.classList.add('row');
  boxBody.appendChild(row);
  for (const [metricName, statistic] of Object.entries(metrics)) {
    $.getJSON(`${metricsUrl}?queue_name=${data.queue_name}&metric_name=${metricName}&statistic=${statistic}`).done(data => {
      renderChart(row, data);
    }).fail(jqxhr => {
      const errorMessage = document.createElement('div');
      errorMessage.classList.add('alert');
      errorMessage.classList.add('alert-danger');
      errorMessage.appendChild(document.createTextNode(`Failed to load SQS metrics ${metricName}: ${jqxhr.status}: ${jqxhr.statusText}`));

      return div.appendChild(errorMessage);
    });
  }
};

const renderChart = function(row, data) {
  const div = document.createElement('div');
  div.classList.add('col-md-3');
  const chartDiv = document.createElement('div');
  div.appendChild(chartDiv);
  div.dataset.label = data.label;

  // Insert charts ordered by label name
  let inserted = false;
  for (const child of row.children) {
    if (data.label < child.dataset.label) {
      row.insertBefore(div, child);
      inserted = true;
      break;
    }
  }
  if (!inserted) {
    row.appendChild(div);
  }

  return Plotly.plot(chartDiv, [{
    type: 'scatter',
    x: data.datapoints.map(point => point.timestamp),
    y: data.datapoints.map(point => point.value),
  }], {
    title: data.label,
  });
};


================================================
FILE: app/assets/stylesheets/barbeque/application.css
================================================
/*
 *= require bootstrap
 *= require AdminLTE
 *= require skins/skin-blue

 *= require barbeque/job_definitions
 */

:root {
  --barbeque-border-color: #ececec;
}

.box-body {
  padding: 12px;
}

.box-header .box-title.with_padding {
  padding: 6px;
  font-size: 22px;
}

.table.table-bordered {
  margin-bottom: 12px;
  border-color: var(--barbeque-border-color);
}

.table.table-bordered td, .table.table-bordered th {
  border-color: var(--barbeque-border-color);
}


================================================
FILE: app/assets/stylesheets/barbeque/job_definitions.css
================================================
.barbeque_job_definitions_controller .use_slack_notification_wrapper label {
  font-weight: normal;
}

.barbeque_job_definitions_controller .use_slack_notification {
  margin: 0 2px 0 8px;
}

.barbeque_job_definitions_controller .slack_notification_field {
  display: none;
}

.barbeque_job_definitions_controller .slack_notification_field label {
  font-weight: normal;
}

.barbeque_job_definitions_controller .slack_notification_field.active {
  display: block;
}

.barbeque_job_definitions_controller .retry_configuration_wrapper label {
  font-weight: normal;
}

.barbeque_job_definitions_controller .enable_retry_configuration {
  margin: 0 2px 0 8px;
}

.barbeque_job_definitions_controller .retry_configuration_field {
  display: none;
}

.barbeque_job_definitions_controller .retry_configuration_field label {
  font-weight: normal;
}

.barbeque_job_definitions_controller .retry_configuration_field.active {
  display: block;
}

.barbeque_job_definitions_controller .table.table-bordered td,
.barbeque_job_definitions_controller .table.table-bordered th {
  overflow-wrap: anywhere;
  min-width: 100px;
}


================================================
FILE: app/controllers/barbeque/api/application_controller.rb
================================================
require 'garage'

class Barbeque::Api::ApplicationController < ActionController::API
  before_action :force_json_format

  include Garage::ControllerHelper

  rescue_from ActiveRecord::RecordNotFound do |exception|
    respond_with_error(404, 'record_not_found', exception.message)
  end

  rescue_from WeakParameters::ValidationError do |exception|
    respond_with_error(400, 'invalid_parameter', exception.message)
  end

  private

  # @param [Integer] status_code HTTP status code
  # @param [String] error_code Must be unique
  # @param [String] message Error message for API client, not for end user.
  def respond_with_error(status_code, error_code, message)
    render json: { status_code: status_code, error_code: error_code, message: message }, status: status_code
  end

  # This is required to use ActionController::API with Garage
  def force_json_format
    request.format = :json
  end
end


================================================
FILE: app/controllers/barbeque/api/job_executions_controller.rb
================================================
require 'barbeque/maintenance'

class Barbeque::Api::JobExecutionsController < Barbeque::Api::ApplicationController
  include Garage::RestfulActions

  validates :create do
    string :application, required: true, description: 'Application name of the job'
    string :job, required: true, description: 'Class of Job to be enqueued'
    string :queue, required: true, description: 'Queue name to enqueue a job'
    any :message, required: true, description: 'Free-format JSON'
    integer :delay_seconds, description: 'Set message timer of SQS'
  end

  rescue_from Barbeque::MessageEnqueuingService::BadRequest do |exc|
    render status: 400, json: { error: exc.message }
  end

  private

  def require_resources
    protect_resource_as Barbeque::Api::JobExecutionResource
  end

  def require_resource
    model = Barbeque::JobExecution.find_or_initialize_by(message_id: params[:message_id])
    @resource = Barbeque::Api::JobExecutionResource.new(model)
  rescue ActiveRecord::StatementInvalid, Mysql2::Error::ConnectionError => e
    if Barbeque::Maintenance.database_maintenance_mode?
      Barbeque::ExceptionHandler.handle_exception(e)
      @resource = Barbeque::Api::DatabaseMaintenanceResource.new(e)
    else
      raise e
    end
  end

  def create_resource
    message_id = enqueue_message
    model = Barbeque::JobExecution.new(message_id: message_id)
    @resource = Barbeque::Api::JobExecutionResource.new(model)
  end

  # @return [String] id of a message queued to SQS.
  def enqueue_message
    Barbeque::MessageEnqueuingService.new(
      application: params[:application],
      job:         params[:job],
      queue:       params[:queue],
      message:     params[:message],
      delay_seconds: params[:delay_seconds],
    ).run
  end

  # Link to job_execution isn't available if it isn't dequeued yet
  def location
    if @resource.id
      super
    else
      nil
    end
  end

  def respond_with_resource_options
    if @resource.is_a?(Barbeque::Api::DatabaseMaintenanceResource)
      super.merge(status: 503)
    else
      super
    end
  end
end


================================================
FILE: app/controllers/barbeque/api/job_retries_controller.rb
================================================
class Barbeque::Api::JobRetriesController < Barbeque::Api::ApplicationController
  include Garage::RestfulActions

  # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html
  SQS_MAX_DELAY_SECONDS = 900

  validates :create do
    integer :delay_seconds, only: 0..SQS_MAX_DELAY_SECONDS
  end

  private

  def require_resources
    protect_resource_as Barbeque::Api::JobRetryResource
  end

  def create_resource
    result = retry_message
    Barbeque::JobRetry.new(message_id: result.message_id).to_resource
  end

  def retry_message
    Barbeque::MessageRetryingService.new(
      message_id: params[:job_execution_message_id],
      delay_seconds: params[:delay_seconds].to_i,
    ).run
  end
end


================================================
FILE: app/controllers/barbeque/api/revision_locks_controller.rb
================================================
class Barbeque::Api::RevisionLocksController < Barbeque::Api::ApplicationController
  include Garage::RestfulActions

  validates :create do
    string :revision, required: true, description: 'Docker image revision to lock'
  end

  private

  def require_resources
    protect_resource_as Barbeque::Api::RevisionLockResource
  end

  def require_resource
    @resource = Barbeque::App.find_by!(name: params[:app_name])
  end

  def create_resource
    app = Barbeque::App.find_by!(name: params[:app_name])
    image = Barbeque::DockerImage.new(app.docker_image)
    image.tag = params[:revision]
    app.update!(docker_image: image.to_s)

    Barbeque::Api::RevisionLockResource.new(app)
  end

  def destroy_resource
    image = Barbeque::DockerImage.new(@resource.docker_image)
    image.tag = 'latest'
    @resource.update!(docker_image: image.to_s)

    Barbeque::Api::RevisionLockResource.new(@resource)
  end
end


================================================
FILE: app/controllers/barbeque/application_controller.rb
================================================
module Barbeque
  class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
  end
end


================================================
FILE: app/controllers/barbeque/apps_controller.rb
================================================
class Barbeque::AppsController < Barbeque::ApplicationController
  def index
    @apps = Barbeque::App.all
  end

  def show
    @app = Barbeque::App.find(params[:id])
  end

  def new
    @app = Barbeque::App.new
  end

  def edit
    @app = Barbeque::App.find(params[:id])
  end

  def create
    @app = Barbeque::App.new(params.require(:app).permit(:name, :docker_image, :description))

    if @app.save
      redirect_to @app, notice: 'App was successfully created.'
    else
      render :new
    end
  end

  def update
    @app = Barbeque::App.find(params[:id])
    # Name can't be changed after it's created.
    if @app.update(params.require(:app).permit(:docker_image, :description))
      redirect_to @app, notice: 'App was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @app = Barbeque::App.find(params[:id])
    @app.destroy
    redirect_to root_path, notice: 'App was successfully destroyed.'
  end
end


================================================
FILE: app/controllers/barbeque/job_definitions_controller.rb
================================================
class Barbeque::JobDefinitionsController < Barbeque::ApplicationController
  def index
    @job_definitions = Barbeque::JobDefinition.all
  end

  def show
    @job_definition = Barbeque::JobDefinition.find(params[:id])
    @job_executions = @job_definition.job_executions.order(id: :desc).page(params[:page])
    @retry_config = @job_definition.retry_config
    @status = params[:status].presence.try(&:to_i)
    if @status
      @job_executions = @job_executions.where(status: @status)
    end
  end

  def new
    @job_definition = Barbeque::JobDefinition.new
    @job_definition.build_slack_notification
    @job_definition.build_retry_config
    if params[:job_definition]
      @job_definition.assign_attributes(new_job_definition_params)
    end
  end

  def edit
    @job_definition = Barbeque::JobDefinition.find(params[:id])
    unless @job_definition.slack_notification
      @job_definition.build_slack_notification
    end
    unless @job_definition.retry_config
      @job_definition.build_retry_config
    end
  end

  def create
    attributes = new_job_definition_params.merge(command: command_array)
    @job_definition = Barbeque::JobDefinition.new(attributes)

    if @job_definition.save
      redirect_to @job_definition, notice: 'Job definition was successfully created.'
    else
      render :new
    end
  end

  def update
    @job_definition = Barbeque::JobDefinition.find(params[:id])
    attributes = params.require(:job_definition).permit(
      :description,
      slack_notification_attributes: slack_notification_params,
      retry_config_attributes: retry_config_params,
    ).merge(command: command_array)
    if @job_definition.update(attributes)
      redirect_to @job_definition, notice: 'Job definition was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @job_definition = Barbeque::JobDefinition.find(params[:id])
    @job_definition.sns_subscriptions.each do |sns_subscription|
      Barbeque::SnsSubscriptionService.new.unsubscribe(sns_subscription)
    end
    @job_definition.destroy
    redirect_to job_definitions_url, notice: 'Job definition was successfully destroyed.'
  end

  def stats
    @job_definition = Barbeque::JobDefinition.find(params[:job_definition_id])
    @days = (params[:days] || 3).to_i
  end

  def execution_stats
    job_definition = Barbeque::JobDefinition.find(params[:job_definition_id])
    days = (params[:days] || 3).to_i
    now = Time.zone.now
    render json: job_definition.execution_stats(days.days.ago(now), now)
  end

  private

  def slack_notification_params
    %i[id channel notify_success notify_failure_only_if_retry_limit_reached failure_notification_text _destroy]
  end

  def retry_config_params
    %i[id retry_limit base_delay max_delay jitter _destroy]
  end

  def command_array
    Shellwords.split(params.require(:job_definition)[:command])
  end

  def new_job_definition_params
    params.require(:job_definition).permit(
      :job,
      :app_id,
      :description,
      slack_notification_attributes: slack_notification_params,
      retry_config_attributes: retry_config_params,
    )
  end
end


================================================
FILE: app/controllers/barbeque/job_executions_controller.rb
================================================
class Barbeque::JobExecutionsController < Barbeque::ApplicationController
  ID_REGEXP = /\A[0-9]+\z/

  def show
    if ID_REGEXP === params[:message_id]
      job_execution = Barbeque::JobExecution.find_by(id: params[:message_id])
      if job_execution
        redirect_to(job_execution)
        return
      end
    end
    @job_execution = Barbeque::JobExecution.find_by!(message_id: params[:message_id])
    # Return 404 when job_definition or app is deleted
    @job_definition = Barbeque::JobDefinition.find(@job_execution.job_definition_id)
    @app = Barbeque::App.find(@job_definition.app_id)
    @log = @job_execution.execution_log
    @job_retries = @job_execution.job_retries.order(id: :desc)
  end

  def retry
    @job_execution = Barbeque::JobExecution.find_by!(message_id: params[:job_execution_message_id])
    raise ActionController::BadRequest unless @job_execution.retryable?

    result = Barbeque::MessageRetryingService.new(message_id: @job_execution.message_id).run
    @job_execution.retried!

    redirect_to @job_execution, notice: "Succeed to retry (message_id=#{result.message_id})"
  end
end


================================================
FILE: app/controllers/barbeque/job_queues_controller.rb
================================================
require 'aws-sdk-cloudwatch'
require 'aws-sdk-sqs'
require 'barbeque/config'

class Barbeque::JobQueuesController < Barbeque::ApplicationController
  def index
    @job_queues = Barbeque::JobQueue.all
  end

  def show
    @job_queue = Barbeque::JobQueue.find(params[:id])
  end

  def new
    @job_queue = Barbeque::JobQueue.new
  end

  def edit
    @job_queue = Barbeque::JobQueue.find(params[:id])
  end

  def create
    @job_queue = Barbeque::JobQueue.new(params.require(:job_queue).permit(:name, :description))
    @job_queue.queue_url = create_queue(@job_queue).queue_url if @job_queue.valid?

    if @job_queue.save
      redirect_to @job_queue, notice: 'Job queue was successfully created.'
    else
      render :new
    end
  end

  def update
    @job_queue = Barbeque::JobQueue.find(params[:id])
    # Name can't be changed after it's created.
    if @job_queue.update(params.require(:job_queue).permit(:description))
      redirect_to @job_queue, notice: 'Job queue was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    @job_queue = Barbeque::JobQueue.find(params[:id])
    @job_queue.destroy
    redirect_to job_queues_url, notice: 'Job queue was successfully destroyed.'
  end

  def sqs_attributes
    job_queue = Barbeque::JobQueue.find(params[:id])
    attributes = self.class.sqs_client.get_queue_attributes(
      queue_url: job_queue.queue_url,
      attribute_names: %w[
        ApproximateNumberOfMessages
        ApproximateNumberOfMessagesNotVisible
        RedrivePolicy
        QueueArn
      ],
    ).attributes
    dlq_metrics =
      if attributes['RedrivePolicy']
        dlq_arn = JSON.parse(attributes['RedrivePolicy']).fetch('deadLetterTargetArn')
        dlq_name = queue_name_from_arn(dlq_arn)
        dlq_url = self.class.sqs_client.get_queue_url(queue_name: dlq_name).queue_url
        dlq_attributes = self.class.sqs_client.get_queue_attributes(
          queue_url: dlq_url,
          attribute_names: %w[
            ApproximateNumberOfMessages
            ApproximateNumberOfMessagesNotVisible
          ],
        ).attributes.transform_values(&:to_i)
        {
          queue_name: dlq_name,
          attributes: dlq_attributes,
        }
      else
        nil
      end
    render json: {
      queue_name: queue_name_from_arn(attributes['QueueArn']),
      attributes: {
        'ApproximateNumberOfMessages' => attributes['ApproximateNumberOfMessages'].to_i,
        'ApproximateNumberOfMessagesNotVisible' => attributes['ApproximateNumberOfMessagesNotVisible'].to_i,
      },
      dlq: dlq_metrics,
    }
  end

  def sqs_metrics
    queue_name = params[:queue_name]
    unless queue_name.present?
      render status: 400, json: { error: 'params[:queue_name] is required' }
      return
    end
    metric_name = params[:metric_name]
    unless metric_name.present?
      render status: 400, json: { error: 'params[:metric_name] is required' }
      return
    end
    statistic = params[:statistic]
    unless statistic.present?
      render status: 400, json: { error: 'params[:statistic] is required' }
      return
    end

    now = Time.zone.now
    from = now - 24.hours
    resp = self.class.cloudwatch_client.get_metric_statistics(
      namespace: 'AWS/SQS',
      metric_name: metric_name,
      dimensions: [
        { name: 'QueueName', value: queue_name },
      ],
      start_time: from,
      end_time: now,
      period: compute_minimum_period(from, now),
      statistics: [statistic],
    )

    render json: {
      label: resp.label,
      datapoints: resp.datapoints.sort_by(&:timestamp).map { |datapoint|
        {
          timestamp: Time.zone.at(datapoint.timestamp),
          value: datapoint[statistic.underscore],
        }
      },
    }
  end

  private

  # @return [Aws::SQS::Types::CreateQueueResult] A struct which has only queue_url.
  def create_queue(job_queue)
    Aws::SQS::Client.new.create_queue(
      queue_name: job_queue.sqs_queue_name,
      attributes: {
        'ReceiveMessageWaitTimeSeconds' => Barbeque.config.sqs_receive_message_wait_time.to_s,
      },
    )
  end

  def self.sqs_client
    @sqs_client ||= Aws::SQS::Client.new
  end

  def self.cloudwatch_client
    @cloudwatch_client ||= Aws::CloudWatch::Client.new
  end

  def queue_name_from_arn(arn)
    arn.slice(/[^:]+\z/)
  end

  def compute_minimum_period(start_time, end_time, maximum_datapoint: 1440)
    # - Datapoints cannot exceed 1,440
    # - Period must be a multiple of 60
    maximum_datapoint = [maximum_datapoint, 1440].min.to_f
    minimum_period = ((end_time - start_time) / maximum_datapoint).ceil
    r = minimum_period % 60
    if r == 0
      minimum_period
    else
      minimum_period - r + 60
    end
  end
end


================================================
FILE: app/controllers/barbeque/job_retries_controller.rb
================================================
class Barbeque::JobRetriesController < Barbeque::ApplicationController
  def show
    @job_retry = Barbeque::JobRetry.find(params[:id])
    @job_execution = @job_retry.job_execution
    # Return 404 when job_definition or app is deleted
    @job_definition = Barbeque::JobDefinition.find(@job_execution.job_definition_id)
    @app = Barbeque::App.find(@job_definition.app_id)

    if params[:job_execution_message_id] != @job_execution.message_id
      redirect_to([@job_execution, @job_retry])
    end

    @execution_log = @job_execution.execution_log
    @retry_log = @job_retry.execution_log
  end
end


================================================
FILE: app/controllers/barbeque/monitors_controller.rb
================================================
class Barbeque::MonitorsController < Barbeque::ApplicationController
  def index
    now = Time.zone.now
    from = 6.hours.ago(now.beginning_of_hour)
    rows = Barbeque::JobExecution.find_by_sql([<<SQL.strip_heredoc, from, now]).map(&:attributes)
    select
      t.date_hour
      , app.id as app_id
      , app.name as app_name
      , def.id as job_id
      , def.job as job_name
      , t.cnt
    from
      (
        select
          date_format(e.created_at, '%Y-%m-%d %H:00:00') as date_hour
          , e.job_definition_id
          , count(1) as cnt
        from #{Barbeque::JobExecution.table_name} e
        where
          e.created_at between ? and ?
        group by
          date_hour
          , e.job_definition_id
      ) t
      inner join #{Barbeque::JobDefinition.table_name} def on def.id = t.job_definition_id
      inner join #{Barbeque::App.table_name} app on app.id = def.app_id
SQL

    jobs = {}
    rows.each do |row|
      job_id = row.fetch('job_id')
      job = {
        app_id: row.fetch('app_id'),
        app_name: row.fetch('app_name'),
        job_id: job_id,
        job_name: row.fetch('job_name'),
      }
      jobs[job_id] = job
    end

    @recently_processed_jobs = {}
    t = from
    while t < now
      @recently_processed_jobs[t] = {}
      jobs.each do |job_id, job|
        @recently_processed_jobs[t][job_id] = job.merge(count: 0)
      end
      t += 1.hour
    end

    rows.each do |row|
      date_hour = Time.zone.parse("#{row.fetch('date_hour')} UTC")
      job_id = row.fetch('job_id')
      @recently_processed_jobs[date_hour][job_id] = jobs[job_id].merge(count: row.fetch('cnt'))
    end
  end
end


================================================
FILE: app/controllers/barbeque/sns_subscriptions_controller.rb
================================================
class Barbeque::SnsSubscriptionsController < Barbeque::ApplicationController
  def index
    @sns_subscriptions = Barbeque::SnsSubscription.all
  end

  def show
    @sns_subscription = Barbeque::SnsSubscription.find(params[:id])
  end

  def new
    @sns_subscription = Barbeque::SnsSubscription.new
  end

  def edit
    @sns_subscription = Barbeque::SnsSubscription.find(params[:id])
  end

  def create
    @sns_subscription = Barbeque::SnsSubscription.new(params.require(:sns_subscription).permit(:topic_arn, :job_queue_id, :job_definition_id))
    if Barbeque::SnsSubscriptionService.new.subscribe(@sns_subscription)
      redirect_to @sns_subscription, notice: 'SNS subscription was successfully created.'
    else
      render :new
    end
  end

  def update
    @sns_subscription = Barbeque::SnsSubscription.find(params[:id])
    if @sns_subscription.update(params.require(:sns_subscription).permit(:job_definition_id))
      redirect_to @sns_subscription, notice: 'SNS subscription was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    sns_subscription = Barbeque::SnsSubscription.find(params[:id])
    Barbeque::SnsSubscriptionService.new.unsubscribe(sns_subscription)
    redirect_to sns_subscriptions_path, notice: 'SNS subscription was successfully destroyed.'
  end
end


================================================
FILE: app/helpers/barbeque/application_helper.rb
================================================
module Barbeque
  module ApplicationHelper
  end
end


================================================
FILE: app/helpers/barbeque/job_definitions_helper.rb
================================================
module Barbeque::JobDefinitionsHelper
  def distance_of_time(from, to)
    return '' if from.nil? || to.nil?

    secs  = (to - from).to_i
    mins  = secs / 60
    hours = mins / 60
    days  = hours / 24

    text = "%02d:%02d:%02d" % [hours % 24, mins % 60, secs % 60]
    text.prepend("#{days}days ") if days > 0
    text
  end
end


================================================
FILE: app/helpers/barbeque/job_executions_helper.rb
================================================
module Barbeque::JobExecutionsHelper
  def status_label(status)
    color =
      case status
      when 'success'
        'success'
      when 'failed'
        'danger'
      when 'retried'
        'warning'
      when 'pending'
        'info'
      when 'error'
        'danger'
      when 'running'
        'info'
      else
        'default'
      end
    content_tag(:span, status.upcase, class: "label label-#{color}")
  end
end


================================================
FILE: app/models/barbeque/api/application_resource.rb
================================================
require 'garage'

class Barbeque::Api::ApplicationResource
  include Garage::Representer
  include Garage::Authorizable

  attr_reader :model

  def initialize(model = nil)
    @model = model
  end
end


================================================
FILE: app/models/barbeque/api/database_maintenance_resource.rb
================================================
class Barbeque::Api::DatabaseMaintenanceResource
  include Garage::Representer

  property :message

  delegate :message, to: :@exception

  def initialize(exception)
    @exception = exception
  end
end


================================================
FILE: app/models/barbeque/api/job_execution_resource.rb
================================================
class Barbeque::Api::JobExecutionResource < Barbeque::Api::ApplicationResource
  property :message_id
  property :status
  property :id
  property :html_url, selectable: true
  property :message, selectable: true

  delegate :message_id, :status, :id, to: :model

  def html_url
    if model.id
      Barbeque::Engine.routes.url_helpers.job_execution_url(model, host: ENV['BARBEQUE_HOST'])
    else
      nil
    end
  end

  def message
    log = @model.execution_log
    if log
      log['message']
    end
  end
end


================================================
FILE: app/models/barbeque/api/job_retry_resource.rb
================================================
class Barbeque::Api::JobRetryResource < Barbeque::Api::ApplicationResource
  property :message_id

  property :status

  delegate :message_id, :status, to: :model
end


================================================
FILE: app/models/barbeque/api/revision_lock_resource.rb
================================================
class Barbeque::Api::RevisionLockResource < Barbeque::Api::ApplicationResource
  property :revision

  def revision
    Barbeque::DockerImage.new(@model.docker_image).tag
  end
end


================================================
FILE: app/models/barbeque/app.rb
================================================
class Barbeque::App < Barbeque::ApplicationRecord
  validates :name, presence: true, uniqueness: true
  validates :docker_image, presence: true

  attr_readonly :name

  has_many :job_definitions, dependent: :destroy
end


================================================
FILE: app/models/barbeque/application_record.rb
================================================
module Barbeque
  class ApplicationRecord < ActiveRecord::Base
    self.abstract_class = true
  end
end


================================================
FILE: app/models/barbeque/docker_container.rb
================================================
class Barbeque::DockerContainer < Barbeque::ApplicationRecord
end


================================================
FILE: app/models/barbeque/ecs_hako_task.rb
================================================
class Barbeque::EcsHakoTask < Barbeque::ApplicationRecord
end


================================================
FILE: app/models/barbeque/job_definition.rb
================================================
class Barbeque::JobDefinition < Barbeque::ApplicationRecord
  belongs_to :app
  has_many :job_executions
  has_many :sns_subscriptions
  has_one :slack_notification, dependent: :destroy
  has_one :retry_config, dependent: :destroy

  validates :job, uniqueness: { scope: :app_id }

  attr_readonly :app_id
  attr_readonly :job

  serialize :command, Array

  accepts_nested_attributes_for :slack_notification, allow_destroy: true
  accepts_nested_attributes_for :retry_config, allow_destroy: true

  DATE_HOUR_SQL = 'date_format(created_at, "%Y-%m-%d %H:00:00")'

  def execution_stats(from, to)
    from = from.beginning_of_hour
    to = to.beginning_of_hour
    stats = Hash.new { |h, k| h[k] = { count: 0, avg_time: 0 } }
    job_executions.where(created_at: from .. to).group(DATE_HOUR_SQL).order(Arel.sql(DATE_HOUR_SQL)).pluck(Arel.sql("#{DATE_HOUR_SQL}, count(1), avg(timestampdiff(second, created_at, finished_at))")).each do |date_hour, count, avg_time|
      time = Time.zone.parse("#{date_hour} UTC")
      stats[time] = {
        count: count,
        avg_time: avg_time,
      }
    end
    (from.to_i ... to.to_i).step(1.hour.to_i).map do |t|
      time = Time.at(t)
      stats[time].merge(date_hour: time)
    end
  end
end


================================================
FILE: app/models/barbeque/job_execution.rb
================================================
class Barbeque::JobExecution < Barbeque::ApplicationRecord
  belongs_to :job_definition
  belongs_to :job_queue
  has_one :slack_notification, through: :job_definition
  has_one :app, through: :job_definition
  has_many :job_retries, dependent: :destroy

  enum status: {
    pending: 0,
    success: 1,
    failed:  2,
    retried: 3,
    error: 4,
    running: 5,
  }

  paginates_per 15

  # @return [Hash] - A hash created by `JobExecutor::Job#log_result`
  def execution_log
    @execution_log ||= Barbeque::ExecutionLog.load(execution: self)
  end

  def retryable?
    failed? || error?
  end

  def to_param
    message_id
  end

  def retry_if_possible!
    unless retryable?
      return
    end
    retry_config = job_definition.retry_config
    unless retry_config
      return
    end

    retries = job_retries.count
    if retry_config.should_retry?(retries)
      delay_seconds = retry_config.delay_seconds(retries).to_i
      Barbeque::MessageRetryingService.new(message_id: message_id, delay_seconds: delay_seconds).run
      retried!
    end
  end
end


================================================
FILE: app/models/barbeque/job_queue.rb
================================================
require 'barbeque/maintenance'

class Barbeque::JobQueue < Barbeque::ApplicationRecord
  SQS_NAME_PREFIX = ENV['BARBEQUE_SQS_NAME_PREFIX'] || 'Barbeque-'
  SQS_NAME_MAX_LENGTH = 80

  has_many :job_executions
  has_many :sns_subscriptions, dependent: :destroy

  # SQS queue allows [a-zA-Z0-9_-]+ as queue name. Its maximum length is 80.
  validates :name, presence: true, uniqueness: true, format: /\A[a-zA-Z0-9_-]+\z/,
    length: { maximum: SQS_NAME_MAX_LENGTH - SQS_NAME_PREFIX.length }

  attr_readonly :name

  def sqs_queue_name
    SQS_NAME_PREFIX + name
  end

  # Returns queue URL of given name.
  # Basically, we should use stored queue URL as the documentation[1] suggests.
  # But when the Barbeque's database is temporarily unavailable due to
  # scheduled maintenance, we have to build queue URL without the database. The
  # maintenance mode is enabled by BARBEQUE_DATABASE_MAINTENANCE and
  # AWS_ACCOUNT_ID variable.
  # [1]: http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-queue-message-identifiers.html#sqs-general-identifiers
  #
  # @param name [String] queue name in Barbeque
  # @return [String] queue URL of SQS
  def self.queue_url_from_name(name)
    if Barbeque::Maintenance.database_maintenance_mode?
      "https://sqs.#{ENV.fetch('AWS_REGION')}.amazonaws.com/#{ENV.fetch('AWS_ACCOUNT_ID')}/#{SQS_NAME_PREFIX}#{name}"
    else
      select(:queue_url).find_by!(name: name).queue_url
    end
  end
end


================================================
FILE: app/models/barbeque/job_retry.rb
================================================
class Barbeque::JobRetry < Barbeque::ApplicationRecord
  belongs_to :job_execution
  has_one :job_definition, through: :job_execution
  has_one :app, through: :job_definition
  has_one :slack_notification, through: :job_execution

  enum status: {
    pending: 0,
    success: 1,
    failed:  2,
    retried: 3,
    error: 4,
    running: 5,
  }

  # @return [Hash] - A hash created by `JobExecutor::Retry#log_result`
  def execution_log
    @execution_log ||= Barbeque::ExecutionLog.load(execution: self)
  end

  def to_resource
    Barbeque::Api::JobRetryResource.new(self)
  end
end


================================================
FILE: app/models/barbeque/retry_config.rb
================================================
class Barbeque::RetryConfig < Barbeque::ApplicationRecord
  belongs_to :job_definition

  validates :retry_limit, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
  validates :base_delay, numericality: { greater_than: 0.0 }, allow_nil: true
  validates :max_delay, numericality: { greater_than: 0 }, allow_nil: true

  def should_retry?(retries)
    retries < retry_limit
  end

  # This algorithm is based on "Exponential Backoff And Jitter" article
  # https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
  def delay_seconds(retries)
    delay = 2 ** retries * base_delay
    if max_delay
      delay = [delay, max_delay].min
    end
    if jitter
      delay = Kernel.rand(0 .. delay)
    end
    delay
  end
end


================================================
FILE: app/models/barbeque/slack_notification.rb
================================================
class Barbeque::SlackNotification < Barbeque::ApplicationRecord
  belongs_to :job_definition, optional: true

  validates :channel, presence: true
end


================================================
FILE: app/models/barbeque/sns_subscription.rb
================================================
module Barbeque
  class SnsSubscription < ApplicationRecord
    belongs_to :job_queue
    belongs_to :job_definition
    has_one :app, through: :job_definition

    validates :topic_arn,
      uniqueness: { scope: :job_queue, message: 'should be set with only one queue' },
      presence: true
    validate :topic_arn_is_formatted

    def topic_region
      Aws::ARNParser.parse(topic_arn).region
    end

    private

    def topic_arn_is_formatted
      unless Aws::ARNParser.arn?(topic_arn)
        errors.add(:topic_arn, 'is not a valid ARN')
      end
    end
  end
end


================================================
FILE: app/models/concerns/.keep
================================================


================================================
FILE: app/services/barbeque/message_enqueuing_service.rb
================================================
require 'aws-sdk-sqs'

class Barbeque::MessageEnqueuingService
  DEFAULT_QUEUE = ENV['BARBEQUE_DEFAULT_QUEUE'] || 'default'
  VERIFY_ENQUEUED_JOBS = ENV['BARBEQUE_VERIFY_ENQUEUED_JOBS'] || '0'

  class BadRequest < StandardError
  end

  def self.sqs_client
    @sqs_client ||= Aws::SQS::Client.new
  end

  # @param [String] application
  # @param [String] job
  # @param [Object] message
  # @param [String] queue
  # @param [Integer, nil] delay_seconds
  def initialize(application:, job:, message:, queue: nil, delay_seconds: nil)
    @application = application
    @job         = job
    @queue       = queue || DEFAULT_QUEUE
    @message     = message
    @delay_seconds = delay_seconds
  end

  # @return [String] message_id
  def run
    queue_url = Barbeque::JobQueue.queue_url_from_name(@queue)
    if VERIFY_ENQUEUED_JOBS == '1'
      unless Barbeque::JobDefinition.joins(:app).merge(Barbeque::App.where(name: @application)).where(job: @job).exists?
        raise BadRequest.new("JobDefinition '#{@job}' isn't defined in '#{@application}' application")
      end
    end
    response = Barbeque::MessageEnqueuingService.sqs_client.send_message(
      queue_url:    queue_url,
      message_body: build_message.to_json,
      delay_seconds: @delay_seconds,
    )
    response.message_id
  rescue Aws::SQS::Errors::InvalidParameterValue => e
    raise BadRequest.new(e.message)
  end

  private

  def build_message
    {
      'Type'        => 'JobExecution',
      'Application' => @application,
      'Job'         => @job,
      'Message'     => @message,
    }
  end
end


================================================
FILE: app/services/barbeque/message_retrying_service.rb
================================================
require 'aws-sdk-sqs'

class Barbeque::MessageRetryingService
  DEFAULT_DELAY_SECONDS = 0

  def self.sqs_client
    @sqs_client ||= Aws::SQS::Client.new
  end

  def initialize(message_id:, delay_seconds: nil)
    @message_id    = message_id
    @delay_seconds = delay_seconds || DEFAULT_DELAY_SECONDS
  end

  def run
    execution = Barbeque::JobExecution.find_by!(message_id: @message_id)
    Barbeque::MessageRetryingService.sqs_client.send_message(
      queue_url:     execution.job_queue.queue_url,
      message_body:  build_message.to_json,
      delay_seconds: @delay_seconds,
    )
  end

  private

  def build_message
    {
      'Type'           => 'JobRetry',
      'RetryMessageId' => @message_id,
    }
  end
end


================================================
FILE: app/services/barbeque/sns_subscription_service.rb
================================================
require 'aws-sdk-sns'
require 'aws-sdk-sqs'

class Barbeque::SnsSubscriptionService
  def self.sqs_client
    @sqs_client ||= Aws::SQS::Client.new
  end

  def self.sns_client(region:)
    return @sns_client[region] if @sns_client
    @sns_client = Hash.new { |hash, region| hash[region] = Aws::SNS::Client.new(region: region) }
    @sns_client[region]
  end

  # @param [Barbeque::SnsSubscription] sns_subscription
  # @return [Boolean] `true` if succeeded to subscribe
  def subscribe(sns_subscription)
    if sns_subscription.valid?
      begin
        subscribe_topic!(sns_subscription)
        sns_subscription.save!
        update_sqs_policy!(sns_subscription)
        true
      rescue Aws::SNS::Errors::AuthorizationError
        sns_subscription.errors.add(:topic_arn, 'is not authorized')
        false
      rescue Aws::SNS::Errors::NotFound
        sns_subscription.errors.add(:topic_arn, 'is not found')
        false
      end
    else
      false
    end
  end

  # @param [Barbeque::SnsSubscription] sns_subscription
  def unsubscribe(sns_subscription)
    sns_subscription.destroy
    update_sqs_policy!(sns_subscription)
    unsubscribe_topic!(sns_subscription)
    nil
  end

  private

  def sqs_client
    Barbeque::SnsSubscriptionService.sqs_client
  end

  def sns_client(region:)
    Barbeque::SnsSubscriptionService.sns_client(region: region)
  end

  # @param [Barbeque::SnsSubscription] sns_subscription
  def update_sqs_policy!(sns_subscription)
    attrs = sqs_client.get_queue_attributes(
      queue_url: sns_subscription.job_queue.queue_url,
      attribute_names: ['QueueArn'],
    )
    queue_arn = attrs.attributes['QueueArn']
    topic_arns = sns_subscription.job_queue.sns_subscriptions.map(&:topic_arn)

    if topic_arns.present?
      policy = generate_policy(queue_arn: queue_arn, topic_arns: topic_arns)
    else
      policy = '' # Be blank when there're no subscriptions.
    end

    sqs_client.set_queue_attributes(
      queue_url: sns_subscription.job_queue.queue_url,
      attributes: { 'Policy' => policy },
    )
  end

  # @param [String] queue_arn
  # @param [Array<String>] topic_arns
  # @return [String] JSON formatted policy
  def generate_policy(queue_arn:, topic_arns:)
    {
      'Version' => '2012-10-17',
      'Statement' => [
        'Effect' => 'Allow',
        'Principal' => '*',
        'Action' => 'sqs:SendMessage',
        'Resource' => queue_arn,
        'Condition' => {
          'ArnEquals' => {
            'aws:SourceArn' => topic_arns,
          }
        }
      ]
    }.to_json
  end

  # @param [Barbeque::SnsSubscription] sns_subscription
  def subscribe_topic!(sns_subscription)
    sqs_attrs = sqs_client.get_queue_attributes(
      queue_url: sns_subscription.job_queue.queue_url,
      attribute_names: ['QueueArn'],
    )
    queue_arn = sqs_attrs.attributes['QueueArn']

    sns_client(region: sns_subscription.topic_region).subscribe(
      topic_arn: sns_subscription.topic_arn,
      protocol: 'sqs',
      endpoint: queue_arn
    )
  end

  # @param [Barbeque::SnsSubscription] sns_subscription
  def unsubscribe_topic!(sns_subscription)
    sqs_attrs = sqs_client.get_queue_attributes(
      queue_url: sns_subscription.job_queue.queue_url,
      attribute_names: ['QueueArn'],
    )
    queue_arn = sqs_attrs.attributes['QueueArn']
    region = sns_subscription.topic_region

    subscriptions = sns_client(region: region).list_subscriptions_by_topic(
      topic_arn: sns_subscription.topic_arn,
    )
    subscription_arn = subscriptions.subscriptions.find {|subscription| subscription.endpoint == queue_arn }.try!(:subscription_arn)

    if subscription_arn
      sns_client(region: region).unsubscribe(
        subscription_arn: subscription_arn,
      )
    end
  end

end


================================================
FILE: app/views/barbeque/apps/_form.html.haml
================================================
.box.box-primary
  .box-header
    %h3.box-title.with_padding
      #{action_name.capitalize} Application

  .box-body
    = form_for @app do |f|
      - if @app.errors.any?
        %strong #{pluralize(@app.errors.count, 'error')} prohibited this application from being saved:
        %ul
          - @app.errors.full_messages.each do |msg|
            %li= msg

      .row.form-group
        .col-md-4
          = f.label :name
          - if @app.persisted?
            -# Name can't be changed after it's created.
            .app_name= @app.name
          - else
            = f.text_field :name, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :docker_image
          = f.text_field :docker_image, class: 'form-control'

      .row.form-group
        .col-md-8
          = f.label :description
          = f.text_area :description, class: 'form-control', rows: 10

      .form-group
        = f.submit 'Save', class: 'btn btn-primary'


================================================
FILE: app/views/barbeque/apps/edit.html.haml
================================================
- content_for(:title, "Edit #{@app.name} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@app.name, app_path(@app.id))

= render 'form'

= link_to 'Back', app_path(@app.id)


================================================
FILE: app/views/barbeque/apps/index.html.haml
================================================
- content_for(:title, 'Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li.active Home

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      All Applications
    = link_to new_app_path, class: 'btn btn-primary pull-right' do
      New Application

  .box-body
    %table.table.table-bordered
      %thead
        %tr
          %th Name
          %th Docker image
          %th Description
          %th

      %tbody
        - @apps.each do |app|
          %tr
            %td= app.name
            %td= app.docker_image
            %td= app.description
            %td
              = link_to 'View Details', app, class: 'btn btn-default btn-sm'


================================================
FILE: app/views/barbeque/apps/new.html.haml
================================================
- content_for(:title, 'New application - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)

= render 'form'

= link_to 'Back', root_path


================================================
FILE: app/views/barbeque/apps/show.html.haml
================================================
- content_for(:title, "#{@app.name} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li.active #{@app.name}

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      Application Details

  .box-body
    %p#notice= notice

    %table.table.table-bordered
      %tbody
        %tr
          %th Name
          %td= @app.name
        %tr
          %th Docker image
          %td= @app.docker_image
        %tr
          %th Description
          %td= @app.description

    = link_to 'Edit', edit_app_path(@app), class: 'btn btn-primary'
    = link_to 'Destroy', app_path(@app),
      class: 'btn', method: :delete, data: { confirm: 'Are you sure to delete application?' }

.box
  .box-header
    %h3.box-title.with_padding Job Definitions
    = link_to 'New Job Definition', new_job_definition_path(job_definition: { app_id: @app.id }), class: 'btn btn-primary pull-right'
  .box-body
    %table.table.table-bordered
      %thead
        %tr
          %th Application
          %th Job
          %th Description
          %th

      %tbody
        - @app.job_definitions.each do |job_definition|
          %tr
            %td= job_definition.app.name
            %td= job_definition.job
            %td= job_definition.description
            %td
              = link_to 'View Details', job_definition, class: 'btn btn-default btn-sm'

= link_to 'Back', root_path


================================================
FILE: app/views/barbeque/job_definitions/_form.html.haml
================================================
.box.box-primary
  .box-header
    %h3.box-title.with_padding
      #{action_name.capitalize} Job Definition

  .box-body
    = form_for @job_definition do |f|
      - if @job_definition.errors.any?
        %strong #{pluralize(@job_definition.errors.count, 'error')} prohibited this job_definition from being saved:
        %ul
          - @job_definition.errors.full_messages.each do |msg|
            %li= msg

      .row.form-group
        .col-md-4
          = f.label :app_id
          - if @job_definition.persisted?
            .job_definition_app_name= @job_definition.app.name
          - else
            = f.collection_select :app_id, Barbeque::App.pluck(:id, :name), :first, :second,
              { prompt: true }, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :job
          - if @job_definition.persisted?
            .job_definition_job= @job_definition.job
          - else
            = f.text_field :job, class: 'form-control', placeholder: 'SomeAsyncJob'

      .row.form-group
        .col-md-8
          = f.label :command
          = f.text_field :command, value: Shellwords.join(@job_definition.command),
            class: 'form-control', placeholder: 'bundle exec rake ...'

      .row.form-group
        .col-md-8
          = f.label :description
          = f.text_area :description, class: 'form-control', rows: 10

      = render 'slack_notification_field', job_definition: @job_definition
      = render 'retry_configuration_field', job_definition: @job_definition

      .form-group
        = f.submit 'Save', class: 'btn btn-primary'


================================================
FILE: app/views/barbeque/job_definitions/_retry_configuration_field.html.haml
================================================
= fields_for(job_definition) do |job_definition_f|
  = job_definition_f.fields_for(:retry_config) do |f|
    = f.hidden_field :id

    .row
      .col-md-8
        %label Retry configuration

    - retry_enabled = f.object.persisted?
    .row.form-group
      .col-md-8.retry_configuration_wrapper
        = f.label :_destroy_true do
          = f.radio_button :_destroy, true, class: 'enable_retry_configuration', checked: !retry_enabled
          No
        = f.label :_destroy_false do
          = f.radio_button :_destroy, false, class: 'enable_retry_configuration', checked: retry_enabled
          Yes

    .retry_configuration_field{ class: ('active' if retry_enabled) }
      .row.form-group
        .col-md-4
          = f.label :retry_limit
          = f.number_field :retry_limit, min: 1, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :base_delay
          = f.number_field :base_delay, min: 0.1, step: 0.1, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :max_delay
          = f.number_field :max_delay, min: 1, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :jitter
          = f.check_box :jitter


================================================
FILE: app/views/barbeque/job_definitions/_slack_notification_field.html.haml
================================================
= fields_for job_definition do |job_definition_f|
  = job_definition_f.fields_for :slack_notification do |f|
    = f.hidden_field :id

    .row
      .col-md-8
        %label Slack Notification

    - use_slack_notification = f.object.persisted?
    .row.form-group
      .col-md-8.use_slack_notification_wrapper
        = f.label :_destroy_true do
          = f.radio_button :_destroy, true, class: 'use_slack_notification', checked: !use_slack_notification
          No
        = f.label :_destroy_false do
          = f.radio_button :_destroy, false, class: 'use_slack_notification', checked: use_slack_notification
          Yes

    .slack_notification_field{ class: ('active' if use_slack_notification) }
      .row.form-group
        .col-md-8
          = f.text_field :channel, placeholder: '#slack-channel', class: 'form-control'

      .row.form-group
        .col-md-8
          = f.check_box :notify_success
          = f.label :notify_success, 'Notify success event to Slack'
        .col-md-8
          = f.check_box :notify_failure_only_if_retry_limit_reached
          = f.label :notify_failure_only_if_retry_limit_reached, 'Notify failure event to Slack only if retry limit reached'

      .row
        .col-md-8
          Failure notification text
      .row.form-group
        .col-md-8
          = f.text_field :failure_notification_text,
            placeholder: '@YourName', class: 'form-control'


================================================
FILE: app/views/barbeque/job_definitions/edit.html.haml
================================================
- content_for(:title, "Edit job definition #{@job_definition.job} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@job_definition.app.name, app_path(@job_definition.app.id))
    %li= link_to(@job_definition.job, job_definition_path(@job_definition.id))

= render 'form'

= link_to 'Back', job_definition_path(@job_definition)


================================================
FILE: app/views/barbeque/job_definitions/index.html.haml
================================================
- content_for(:title, 'Job definitions - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      All Job Definitions
    = link_to new_job_definition_path, class: 'btn btn-primary pull-right' do
      New Job Definition

  .box-body
    %table.table.table-bordered
      %thead
        %tr
          %th Application
          %th Job
          %th Description
          %th

      %tbody
        - @job_definitions.each do |job_definition|
          %tr
            %td= link_to job_definition.app.name, app_path(job_definition.app.id)
            %td= job_definition.job
            %td= job_definition.description
            %td
              = link_to 'View Details', job_definition, class: 'btn btn-default btn-sm'


================================================
FILE: app/views/barbeque/job_definitions/new.html.haml
================================================
- content_for(:title, 'New job definition - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    - if @job_definition.app
      %li= link_to(@job_definition.app.name, app_path(@job_definition.app.id))

= render 'form'

= link_to 'Back', job_definitions_path


================================================
FILE: app/views/barbeque/job_definitions/show.html.haml
================================================
- content_for(:title, "#{@job_definition.job} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@job_definition.app.name, app_path(@job_definition.app.id))
    %li.active #{@job_definition.job}

.row
  .col-sm-7
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Job Definition Details

      .box-body
        %p#notice= notice

        %table.table.table-bordered
          %tbody
            %tr
              %th Application
              %td= link_to(@job_definition.app.name, app_path(@job_definition.app.id))
            %tr
              %th Job
              %td= @job_definition.job
            %tr
              %th Command
              %td= Shellwords.join(@job_definition.command)
            %tr
              %th Description
              %td= @job_definition.description
            %tr
              %th Slack Notification
              %td
                - if @job_definition.slack_notification
                  Yes (#{@job_definition.slack_notification.channel})
                - else
                  No
            %tr
              %th Retry configuration
              %td
                - if @retry_config
                  Yes (retry_limit=#{@retry_config.retry_limit} base_delay=#{@retry_config.base_delay} max_delay=#{@retry_config.max_delay || '+inf'} jitter=#{@retry_config.jitter ? 'enabled' : 'disabled'})
                - else
                  No

        = link_to 'Edit', edit_job_definition_path(@job_definition), class: 'btn btn-primary'
        = link_to 'Destroy', job_definition_path(@job_definition),
          class: 'btn', method: :delete, data: { confirm: 'Are you sure to delete job definition?' }

  .col-sm-5
    .box.box-primary
      .box-header
        %h3.box-title.with_padding.pull-left
          Job Executions
        = link_to(job_definition_stats_path(@job_definition), class: 'btn btn-default pull-right') do
          %i.fa.fa-line-chart
          Statistics

      .box-body
        .nav-tabs-custom
          %ul.nav.nav-tabs
            %li.dropdown
              %a.dropdown-toggle{href: '#', data: { toggle: 'dropdown' }}
                Filter by status
                %span.caret
              %ul.dropdown-menu
                - Barbeque::JobExecution.statuses.each do |name, value|
                  %li{role: 'presentation'}
                    = link_to(url_for(status: value), role: 'menuitem', class: 'active') do
                      - if value == @status
                        %b= name
                      - else
                        = name
        - if @job_executions.present?
          %table.table.table-bordered
            %thead
              %tr
                %th ID
                %th Status
                %th Started At
                %th Elapsed Time
                %th
            %tbody
              - @job_executions.each do |job_execution|
                %tr
                  %td= job_execution.id
                  %td= status_label(job_execution.status)
                  %td= l(job_execution.created_at, format: :short)
                  %td= distance_of_time(job_execution.created_at, job_execution.finished_at)
                  %td
                    .btn-group
                      = link_to job_execution_path(job_execution), class: 'btn btn-default btn-sm btn-flat' do
                        %i.fa.fa-chevron-right
                        Details
                      - if job_execution.retryable?
                        = link_to job_execution_retry_path(job_execution), method: :post, class: 'btn btn-default btn-sm btn-flat',
                          data: { disable_with: 'retrying...', confirm: "Are you sure to retry #{@job_definition.job} ##{job_execution.id}?" } do
                          %i.fa.fa-refresh
                          Retry
          .row
            .col-xs-4
              = link_to path_to_prev_page(@job_executions),
                class: "btn btn-default btn-sm btn-block #{'disabled' unless @job_executions.prev_page}" do
                %i.fa.fa-angle-left
                New
            .col-xs-4.col-xs-offset-4
              = link_to path_to_next_page(@job_executions),
                class: "btn btn-default btn-sm btn-block #{'disabled' unless @job_executions.next_page}" do
                Old
                %i.fa.fa-angle-right

        - else
          No job execution

= link_to 'Back', job_definitions_path


================================================
FILE: app/views/barbeque/job_definitions/stats.html.haml
================================================
- content_for(:title, "#{@job_definition.job} statistics - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@job_definition.app.name, app_path(@job_definition.app.id))
    %li= link_to(@job_definition.job, job_definition_path(@job_definition.id))

.row
  .col-sm-12
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          %i.fa.fa-line-chart
          Execution statistics for #{@days} #{'day'.pluralize(@days)}
      .box-body
        = form_tag(job_definition_stats_path(@job_definition), method: :get, class: 'form-inline') do
          .form-group
            .input-group
              = text_field_tag :days, @days, class: 'form-control'
              .input-group-addon
                days
          = submit_tag 'Go', class: 'btn btn-default'
        #execution-count-chart
        #execution-time-chart

= link_to 'Back', job_definition_path(@job_definition)

:javascript
  jQuery(function($) {
    $.getJSON('#{job_definition_execution_stats_path(@job_definition, days: @days)}').then(stats => {
      const countDiv = document.getElementById('execution-count-chart');
      const timeDiv = document.getElementById('execution-time-chart');

      const date_hours = stats.map(stat => stat.date_hour);
      const counts = stats.map(stat => stat.count);
      const avg_times = stats.map(stat => stat.avg_time);

      Plotly.plot(countDiv, [
        {
          type: 'scatter',
          name: 'Number of executions',
          x: date_hours,
          y: counts,
        },
      ], {
        title: 'Number of executions (hourly)',
      });
      Plotly.plot(timeDiv, [
        {
          type: 'scatter',
          name: 'Average execution time',
          x: date_hours,
          y: avg_times,
        },
      ], {
        title: 'Average execution time (hourly)',
      });
    });
  });


================================================
FILE: app/views/barbeque/job_executions/show.html.haml
================================================
- content_for(:title, "Job execution ##{@job_execution.id} of #{@job_definition.job} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@app.name, app_path(@app.id))
    %li= link_to(@job_definition.job, job_definition_path(@job_definition.id))
    %li.active #{@job_execution.message_id}

.row
  .col-sm-7
    .box.box-primary
      .box-header
        .row
          .col-md-10
            %h3.box-title.with_padding
              \##{@job_execution.id} of
              = link_to @job_definition do
                #{@job_definition.job}
          - if @job_execution.retryable?
            .col-md-2
              = link_to job_execution_retry_path(@job_execution), method: :post, class: 'btn btn-default btn-block pull-right',
                data: { disable_with: 'retrying...', confirm: "Are you sure to retry #{@job_definition.job} ##{@job_execution.id}?" } do
                Retry

      .box-body
        %table.table.table-bordered
          %tbody
            %tr
              %th Execution ID
              %td= @job_execution.id
            %tr
              %th Status
              %td= status_label(@job_execution.status)
            %tr
              %th Created at
              %td= @job_execution.created_at
            %tr
              %th Finished at
              %td= @job_execution.finished_at
            %tr
              %th Elapsed time
              %td= distance_of_time(@job_execution.created_at, @job_execution.finished_at)
            %tr
              %th Message ID
              %td= @job_execution.message_id
            %tr
              %th Message
              %td
                - if @log
                  %pre
                    %code= @log['message']
                - else
                  Log was not found.
  .col-sm-5
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Retries

      .box-body
        - if @job_retries.present?
          %table.table.table-bordered
            %thead
              %tr
                %th ID
                %th Status
                %th Started At
                %th Elapsed Time
                %th
            %tbody
              - @job_retries.each do |job_retry|
                %tr
                  %td= job_retry.id
                  %td= status_label(job_retry.status)
                  %td= l(job_retry.created_at, format: :short)
                  %td= distance_of_time(job_retry.created_at, job_retry.finished_at)
                  %td
                    .btn-group
                      = link_to job_execution_job_retry_path(@job_execution, job_retry), class: 'btn btn-default btn-sm btn-flat' do
                        %i.fa.fa-chevron-right
                        Details
        - else
          Not retried.

.row
  .col-sm-6
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Stdout

      .box-body
        - if @log
          %pre= Rinku.auto_link(html_escape(@log['stdout'])).html_safe
        - else
          Log was not found.

  .col-sm-6
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Stderr

      .box-body
        - if @log
          %pre= Rinku.auto_link(html_escape(@log['stderr'])).html_safe
        - else
          Log was not found.

= link_to 'Back', job_definition_path(@job_definition)


================================================
FILE: app/views/barbeque/job_queues/_form.html.haml
================================================
.box.box-primary
  .box-header
    %h3.box-title.with_padding
      #{action_name.capitalize} Job Queue

  .box-body
    = form_for @job_queue do |f|
      - if @job_queue.errors.any?
        %strong #{pluralize(@job_queue.errors.count, 'error')} prohibited this job_queue from being saved:
        %ul
          - @job_queue.errors.full_messages.each do |msg|
            %li= msg

      .row.form-group
        .col-md-4
          = f.label :name
          - if @job_queue.persisted?
            -# Name can't be changed after it's created.
            .job_queue_name= @job_queue.name
          - else
            = f.text_field :name, class: 'form-control'

      .row.form-group
        .col-md-8
          = f.label :description
          = f.text_area :description, class: 'form-control', rows: 10

      .form-group
        = f.submit 'Save', class: 'btn btn-primary'


================================================
FILE: app/views/barbeque/job_queues/edit.html.haml
================================================
- content_for(:title, "Edit #{@job_queue.name} job queue - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('Job queues', job_queues_path)
    %li= link_to(@job_queue.name, job_queue_path(@job_queue.id))

= render 'form'

= link_to 'Back', job_queue_path(@job_queue.id)


================================================
FILE: app/views/barbeque/job_queues/index.html.haml
================================================
- content_for(:title, 'Job queues - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li.active Job queues

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      All Job Queues
    = link_to new_job_queue_path, class: 'btn btn-primary pull-right' do
      New Job Queue

  .box-body
    %table.table.table-bordered
      %thead
        %tr
          %th Name
          %th Description
          %th

      %tbody
        - @job_queues.each do |job_queue|
          %tr
            %td= job_queue.name
            %td= job_queue.description
            %td
              = link_to 'View Details', job_queue, class: 'btn btn-default btn-sm'


================================================
FILE: app/views/barbeque/job_queues/new.html.haml
================================================
- content_for(:title, 'New job queue - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('Job queues', job_queues_path)

= render 'form'

= link_to 'Back', job_queues_path


================================================
FILE: app/views/barbeque/job_queues/show.html.haml
================================================
- content_for(:title, "#{@job_queue.name} job queue - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('Job queues', job_queues_path)
    %li.active= @job_queue.name

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      Job Queue Details

  .box-body
    %p#notice= notice

    %table.table.table-bordered
      %tbody
        %tr
          %th Name
          %td= @job_queue.name
        %tr
          %th Description
          %td= @job_queue.description

    = link_to 'Edit', edit_job_queue_path(@job_queue), class: 'btn btn-primary'
    = link_to 'Destroy', job_queue_path(@job_queue),
      class: 'btn', method: :delete, data: { confirm: 'Are you sure to delete queue?' }

#sqs-attributes{data: {url: sqs_attributes_job_queue_url(@job_queue.id), metrics_url: sqs_metrics_job_queues_url}}
  %i.loading-indicator.fa.fa-spinner.fa-spin
#sqs-dlq-attributes

= link_to 'Back', job_queues_path


================================================
FILE: app/views/barbeque/job_retries/show.html.haml
================================================
- content_for(:title, "Job retry ##{@job_retry.id} of #{@job_definition.job} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to(@app.name, app_path(@app.id))
    %li= link_to(@job_definition.job, job_definition_path(@job_definition.id))
    %li= link_to(@job_execution.message_id, job_execution_path(@job_execution))
    %li.active ##{@job_retry.id}

.row
  .col-sm-12
    .box.box-primary
      .box-header
        .row
          .col-md-10
            %h3.box-title.with_padding
              Retry ##{@job_retry.id} of
              = link_to @job_definition do
                #{@job_definition.job}
              = link_to @job_execution do
                \##{@job_execution.id}

      .box-body
        %table.table.table-bordered
          %tbody
            %tr
              %th Execution ID
              %td= @job_retry.id
            %tr
              %th Status
              %td= status_label(@job_retry.status)
            %tr
              %th Created at
              %td= @job_retry.created_at
            %tr
              %th Finished at
              %td= @job_retry.finished_at
            %tr
              %th Elapsed time
              %td= distance_of_time(@job_retry.created_at, @job_retry.finished_at)
            %tr
              %th Message ID
              %td= @job_retry.message_id
            %tr
              %th Message
              %td
                - if @execution_log
                  %pre
                    %code= @execution_log['message']
                - else
                  Execution log was not found.

.row
  .col-sm-6
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Stdout

      .box-body
        - if @retry_log
          %pre= Rinku.auto_link(html_escape(@retry_log['stdout'])).html_safe
        - else
          Retry log was not found.

  .col-sm-6
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          Stderr

      .box-body
        - if @retry_log
          %pre= Rinku.auto_link(html_escape(@retry_log['stderr'])).html_safe
        - else
          Retry log was not found.

= link_to 'Back', job_execution_path(@job_execution)


================================================
FILE: app/views/barbeque/monitors/index.html.haml
================================================
- content_for(:title, 'Monitors - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li.active Monitors

.row
  .col-sm-12
    .box.box-primary
      .box-header
        %h3.box-title.with_padding Recently processed jobs (hourly)
      .box-body
        #recently-processed-jobs-chart{data: { jobs: @recently_processed_jobs.to_json }}
        %table.table
          %thead
            %tr
              %th Hour
              %th Application
              %th Job
              %th Count
          %tbody
            - @recently_processed_jobs.keys.sort { |x, y| y <=> x }.each do |date_hour|
              - jobs = @recently_processed_jobs[date_hour]
              - jobs.keys.sort.each do |job_id|
                - job = jobs[job_id]
                %tr
                  %td= date_hour
                  %td= link_to(job[:app_name], app_path(job[:app_id]))
                  %td= link_to(job[:job_name], job_definition_path(job[:job_id]))
                  %td= job[:count]

:javascript
  const chartDiv = document.getElementById('recently-processed-jobs-chart');
  const recentlyProcesedJobs = JSON.parse(chartDiv.dataset.jobs);

  const countsPerJob = {};
  for (const [dateHour, jobs] of Object.entries(recentlyProcesedJobs)) {
    for (const job of Object.values(jobs)) {
      const name = job.app_name + ' - ' + job.job_name;
      if (!countsPerJob[name]) { countsPerJob[name] = []; }
      if (!countsPerJob[name][dateHour]) { countsPerJob[name][dateHour] = job.count; }
    }
  }

  const plotlyArgs = [];
  for (const [name, series] of Object.entries(countsPerJob)) {
    const x = [];
    const y = [];
    for (const [dateHour, count] of Object.entries(series)) {
      x.push(dateHour);
      y.push(count);
    }
    plotlyArgs.push({
      type: 'scatter',
      name,
      x,
      y,
      hoverlabel: {
        namelength: -1,
      }
    });
  }
  Plotly.plot(chartDiv, plotlyArgs);


================================================
FILE: app/views/barbeque/sns_subscriptions/_form.html.haml
================================================
.box.box-primary
  .box-header
    %h3.box-title.with_padding
      #{action_name.capitalize} SNS Subscription

  .box-body
    = form_for @sns_subscription do |f|
      - if @sns_subscription.errors.any?
        %strong #{pluralize(@sns_subscription.errors.count, 'error')} prohibited this SNS subscription from being saved:
        %ul
          - @sns_subscription.errors.full_messages.each do |msg|
            %li= msg

      .row.form-group
        .col-md-8
          = f.label :topic_arn
          - if @sns_subscription.persisted?
            .sns_subscription_topic_arn= @sns_subscription.topic_arn
          - else
            = f.text_field :topic_arn, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :job_queue_id
          - if @sns_subscription.persisted?
            .sns_subscription_job_queue_id= link_to @sns_subscription.job_queue.name, @sns_subscription.job_queue
          - else
            = f.collection_select :job_queue_id, Barbeque::JobQueue.pluck(:id, :name), :first, :second,
              { prompt: true }, class: 'form-control'

      .row.form-group
        .col-md-4
          = f.label :job_definition_id
          = f.collection_select :job_definition_id, Barbeque::JobDefinition.includes(:app).all.map {|definition| [definition.id, "#{definition.app.name} - #{definition.job}"] }, :first, :second,
            { prompt: true }, class: 'form-control'

      .form-group
        = f.submit 'Save', class: 'btn btn-primary'


================================================
FILE: app/views/barbeque/sns_subscriptions/edit.html.haml
================================================
- content_for(:title, "Edit SNS subscription between #{@sns_subscription.topic_arn} and #{@sns_subscription.job_queue.name} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('SNS subscriptions', sns_subscriptions_path)
    %li= link_to(@sns_subscription.topic_arn, sns_subscription_path(@sns_subscription.id))

= render 'form'

= link_to 'Back', sns_subscription_path(@sns_subscription)


================================================
FILE: app/views/barbeque/sns_subscriptions/index.html.haml
================================================
- content_for(:title, 'SNS subscriptions - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li.active SNS subscriptions

.box.box-primary
  .box-header
    %h3.box-title.with_padding
      All SNS Subscriptions
    = link_to new_sns_subscription_path, class: 'btn btn-primary pull-right' do
      New SNS Subscriptions

  .box-body
    %table.table.table-bordered
      %thead
        %tr
          %th topic_arn
          %th Job Queue
          %th Job Definition
          %th

      %tbody
        - @sns_subscriptions.each do |sns_subscription|
          %tr
            %td= sns_subscription.topic_arn
            %td= sns_subscription.job_queue.name
            %td= sns_subscription.job_definition.job
            %td
              = link_to 'View Details', sns_subscription, class: 'btn btn-default btn-sm'


================================================
FILE: app/views/barbeque/sns_subscriptions/new.html.haml
================================================
- content_for(:title, 'New SNS subscription - Barbeque')
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('SNS subscriptions', sns_subscriptions_path)

= render 'form'

= link_to 'Back', sns_subscriptions_path


================================================
FILE: app/views/barbeque/sns_subscriptions/show.html.haml
================================================
- content_for(:title, "SNS subscription between #{@sns_subscription.topic_arn} and #{@sns_subscription.job_queue.name} - Barbeque")
- content_for(:header) do
  %ol.breadcrumb
    %li= link_to('Home', root_path)
    %li= link_to('SNS subscriptions', sns_subscriptions_path)
    %li.active #{@sns_subscription.topic_arn}

.row
  .col-sm-7
    .box.box-primary
      .box-header
        %h3.box-title.with_padding
          SNS Subscription Details

      .box-body
        %p#notice= notice

        %table.table.table-bordered
          %tbody
            %tr
              %th topic_arn
              %td= @sns_subscription.topic_arn
            %tr
              %th Job Queue
              %td= link_to @sns_subscription.job_queue.name, @sns_subscription.job_queue
            %tr
              %th Job Defenition
              %td= link_to @sns_subscription.job_definition.job, @sns_subscription.job_definition
            %tr

        = link_to 'Edit', edit_sns_subscription_path(@sns_subscription), class: 'btn btn-primary'
        = link_to 'Destroy', sns_subscription_path(@sns_subscription),
          class: 'btn', method: :delete, data: { confirm: 'Are you sure to delete SNS subscription?' }

= link_to 'Back', sns_subscriptions_path


================================================
FILE: app/views/layouts/barbeque/_header.html.haml
================================================
%header.main-header
  %a.logo{ href: '/' }
    %span.logo-mini
      %b> BBQ
    %span.logo-lg
      %b> Barbeque

  %nav.navbar.navbar-static-top{ role: 'navigation' }
    %a.sidebar-toggle{ 'data-toggle': 'offcanvas', href: '#', role: 'button' }
      %span.sr-only Toggle navigation


================================================
FILE: app/views/layouts/barbeque/_sidebar.html.haml
================================================
%aside.main-sidebar
  %section.sidebar
    %ul.sidebar-menu
      %li.header Menu
      %li{ class: ('active' if controller_path == 'barbeque/apps') }
        = link_to root_path do
          %i.fa.fa-users
          %span Applications
      %li{ class: ('active' if controller_path == 'barbeque/job_definitions') }
        = link_to job_definitions_path do
          %i.fa.fa-tasks
          %span Job Definitions
      %li{ class: ('active' if controller_path == 'barbeque/job_queues') }
        = link_to job_queues_path do
          %i.fa.fa-cubes
          %span Queues
      %li{ class: ('active' if controller_path == 'barbeque/sns_subscriptions') }
        = link_to sns_subscriptions_path do
          %i.fa.fa-calendar-plus-o
          %span SNS Subscriptions
      %li{ class: ('active' if controller_path == 'barbeque/monitors') }
        = link_to monitors_path do
          %i.fa.fa-tachometer
          %span Monitors


================================================
FILE: app/views/layouts/barbeque/application.html.haml
================================================
!!!
%html
  %head
    %meta{ content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type' }
    %meta{ charset: 'UTF-8' }
    %title= content_for(:title)
    %meta{ content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no', name: 'viewport' }
    -# FIXME: Don't rely on others' CDN
    %link{ href: 'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', rel: 'stylesheet', type: 'text/css' }
    %link{ href: 'https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css', rel: 'stylesheet', type: 'text/css' }

    -# https://github.com/ColorlibHQ/AdminLTE/commit/235481d1d6324c1d4257f86c621700aa7f4a0b7f
    %link{ href: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic', rel: 'stylesheet', type: 'text/css' }

    = stylesheet_link_tag    'barbeque/application', media: 'all'
    = javascript_include_tag 'barbeque/application'
    = csrf_meta_tags

  %body.skin-blue.sidebar-mini{ class: "#{controller_path.parameterize(separator: '_')}_controller #{action_name}_action" }
    .wrapper
      = render partial: 'layouts/barbeque/header'
      = render partial: 'layouts/barbeque/sidebar'
      .content-wrapper
        - if content_for?(:header)
          %section.content-header
            = content_for(:header)
        %section.content
          = yield


================================================
FILE: app/views/layouts/barbeque/apps.html.haml
================================================
- content_for(:header) do
  %h1
    .fa.fa-users
    Applications

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/job_definitions.html.haml
================================================
- content_for(:header) do
  %h1
    .fa.fa-tasks
    Job Definitions

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/job_executions.html.haml
================================================
- content_for(:header) do
  %h1
    Job Executions

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/job_queues.html.haml
================================================
- content_for(:header) do
  %h1
    .fa.fa-cubes
    Queues

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/job_retries.html.haml
================================================
- content_for(:header) do
  %h1
    Job Retries

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/monitors.html.haml
================================================
- content_for(:header) do
  %h1
    .fa.fa-tachometer
    Monitors

= render template: 'layouts/barbeque/application'


================================================
FILE: app/views/layouts/barbeque/sns_subscriptions.html.haml
================================================
- content_for(:header) do
  %h1
    %i.fa.fa-calendar-plus-o
    SNS Subscriptions

= render template: 'layouts/barbeque/application'


================================================
FILE: barbeque.gemspec
================================================
$:.push File.expand_path("../lib", __FILE__)
require "barbeque/version"

Gem::Specification.new do |s|
  s.name        = "barbeque"
  s.version     = Barbeque::VERSION
  s.authors     = ["Takashi Kokubun"]
  s.email       = ["takashi-kokubun@cookpad.com"]
  s.homepage    = "https://github.com/cookpad/barbeque"
  s.summary     = "Job queue system to run job with Docker"
  s.description = "Job queue system to run job with Docker"
  s.license     = "MIT"

  s.files = Dir["{app,config,db,lib,vendor}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]

  s.required_ruby_version = '>= 3.0.0'

  s.add_dependency "aws-sdk-cloudwatch"
  s.add_dependency "aws-sdk-ecs"
  s.add_dependency "aws-sdk-s3"
  s.add_dependency "aws-sdk-sns"
  s.add_dependency "aws-sdk-sqs"
  s.add_dependency "hamlit"
  s.add_dependency "jquery-rails"
  s.add_dependency "kaminari", ">= 1.2.1"
  s.add_dependency "rails", "~> 7.0.5"
  s.add_dependency "rinku"
  s.add_dependency "serverengine"
  s.add_dependency "sprockets-rails"
  s.add_dependency "the_garage"
  s.add_dependency "uglifier"
  s.add_dependency "weak_parameters"

  s.add_development_dependency "autodoc"
  s.add_development_dependency "factory_bot_rails", ">= 5.0.0"
  s.add_development_dependency "listen"
  s.add_development_dependency "mysql2"
  s.add_development_dependency "rspec-rails"
end


================================================
FILE: bin/rails
================================================
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.

ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/barbeque/engine', __FILE__)

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

require 'rails/all'
require 'rails/engine/commands'


================================================
FILE: compose.yaml
================================================
services:
  db:
    image: mysql:8.0
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: barbeque_root
      MYSQL_USER: barbeque
      MYSQL_PASSWORD: barbeque
      MYSQL_DATABASE: barbeque


================================================
FILE: config/initializers/garage.rb
================================================
require 'garage'

Garage.configure {}
Garage.configuration.strategy = Garage::Strategy::NoAuthentication


================================================
FILE: config/routes.rb
================================================
Barbeque::Engine.routes.draw do
  root to: 'apps#index'

  resources :apps, except: :index

  resources :job_definitions do
    get :stats
    get :execution_stats
  end

  resources :job_executions, only: :show, param: :message_id do
    post :retry

    resources :job_retries, only: :show
  end

  resources :job_queues do
    member do
      get :sqs_attributes
    end

    collection do
      get :sqs_metrics
    end
  end

  resources :sns_subscriptions

  resources :monitors, only: %i[index]

  scope :v1, module: 'api', as: :v1 do
    resources :apps, only: [], param: :name, constraints: { name: /[\w-]+/ } do
      resource :revision_lock, only: [:create, :destroy]
    end

    resources :job_executions, only: :show, param: :message_id,
      constraints: { message_id: /[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}/ } do
      resources :job_retries, only: [:create], path: 'retries'
    end
  end

  scope :v2, module: 'api', as: :v2 do
    resources :job_executions, only: :create, param: :message_id,
      constraints: { message_id: /[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}/ }
  end
end


================================================
FILE: db/migrate/20160217020910_create_job_queues.rb
================================================
class CreateJobQueues < ActiveRecord::Migration[5.0]
  def change
    create_table :job_queues, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :name, null: false
      t.text :description
      t.string :queue_url, null: false

      t.timestamps
    end
    add_index :job_queues, [:name], unique: true
  end
end


================================================
FILE: db/migrate/20160219010912_create_job_executions.rb
================================================
class CreateJobExecutions < ActiveRecord::Migration[5.0]
  def change
    create_table :job_executions, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :message_id, null: false
      t.integer :status, null: false, default: 0

      t.timestamps
    end
    add_index :job_executions, [:message_id], unique: true
  end
end


================================================
FILE: db/migrate/20160223060807_create_apps.rb
================================================
class CreateApps < ActiveRecord::Migration[5.0]
  def change
    create_table :apps, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :name, null: false
      t.string :docker_image, null: false
      t.text :description

      t.timestamps
    end
    add_index :apps, [:name], unique: true
  end
end


================================================
FILE: db/migrate/20160223183348_create_job_definitions.rb
================================================
class CreateJobDefinitions < ActiveRecord::Migration[5.0]
  def change
    create_table :job_definitions, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :job, null: false
      t.integer :app_id, null: false
      t.string :command, null: false
      t.text :description

      t.timestamps
    end
    add_index :job_definitions, [:job, :app_id], unique: true
  end
end


================================================
FILE: db/migrate/20160225020801_add_job_definition_id_to_job_executions.rb
================================================
class AddJobDefinitionIdToJobExecutions < ActiveRecord::Migration[5.0]
  def change
    add_column :job_executions, :job_definition_id, :integer
    add_index :job_executions, [:job_definition_id]
  end
end


================================================
FILE: db/migrate/20160412083604_add_finished_at_to_job_executions.rb
================================================
class AddFinishedAtToJobExecutions < ActiveRecord::Migration[5.0]
  def change
    add_column :job_executions, :finished_at, :datetime
  end
end


================================================
FILE: db/migrate/20160415043427_create_slack_notifications.rb
================================================
class CreateSlackNotifications < ActiveRecord::Migration[5.0]
  def change
    create_table :slack_notifications, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.integer :job_definition_id
      t.string :channel, null: false
      t.boolean :notify_success, default: false, null: false
      t.string :failure_notification_text

      t.timestamps
    end
  end
end


================================================
FILE: db/migrate/20160509041452_add_job_queue_id_to_job_executions.rb
================================================
class AddJobQueueIdToJobExecutions < ActiveRecord::Migration[5.0]
  def change
    add_column :job_executions, :job_queue_id, :integer
  end
end


================================================
FILE: db/migrate/20160516041710_create_job_retries.rb
================================================
class CreateJobRetries < ActiveRecord::Migration[5.0]
  def change
    create_table :job_retries, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :message_id, null: false
      t.integer :job_execution_id, null: false
      t.integer :status, null: false, default: 0
      t.datetime :finished_at

      t.timestamps
    end
    add_index :job_retries, [:message_id], unique: true
  end
end


================================================
FILE: db/migrate/20160829023237_prefix_barbeque_to_tables.rb
================================================
class PrefixBarbequeToTables < ActiveRecord::Migration[5.0]
  def change
    rename_table :apps, :barbeque_apps
    rename_table :job_definitions, :barbeque_job_definitions
    rename_table :job_executions, :barbeque_job_executions
    rename_table :job_queues, :barbeque_job_queues
    rename_table :job_retries, :barbeque_job_retries
    rename_table :slack_notifications, :barbeque_slack_notifications
  end
end


================================================
FILE: db/migrate/20170420030157_create_barbeque_sns_subscriptions.rb
================================================
class CreateBarbequeSnsSubscriptions < ActiveRecord::Migration[5.0]
  def change
    create_table :barbeque_sns_subscriptions do |t|
      t.string :topic_arn, null: false
      t.integer :job_queue_id, null: false
      t.integer :job_definition_id, null: false

      t.timestamps
    end
  end
end


================================================
FILE: db/migrate/20170711085157_create_barbeque_docker_containers.rb
================================================
class CreateBarbequeDockerContainers < ActiveRecord::Migration[5.0]
  def change
    create_table :barbeque_docker_containers, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :message_id, null: false
      t.string :container_id, null: false

      t.timestamps

      t.index ['message_id'], unique: true
    end
  end
end


================================================
FILE: db/migrate/20170712075449_create_barbeque_ecs_hako_tasks.rb
================================================
class CreateBarbequeEcsHakoTasks < ActiveRecord::Migration[5.0]
  def change
    create_table :barbeque_ecs_hako_tasks, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.string :message_id, null: false
      t.string :cluster, null: false
      t.string :task_arn, null: false

      t.timestamps

      t.index ['message_id'], unique: true
    end
  end
end


================================================
FILE: db/migrate/20170724025542_add_index_to_job_execution_status.rb
================================================
class AddIndexToJobExecutionStatus < ActiveRecord::Migration[5.0]
  def change
    add_index :barbeque_job_executions, :status
    add_index :barbeque_job_retries, :status
  end
end


================================================
FILE: db/migrate/20180411070937_add_index_to_barbeque_job_executions_created_at.rb
================================================
class AddIndexToBarbequeJobExecutionsCreatedAt < ActiveRecord::Migration[5.1]
  def change
    add_index :barbeque_job_executions, :created_at
  end
end


================================================
FILE: db/migrate/20190221050714_create_barbeque_retry_configs.rb
================================================
class CreateBarbequeRetryConfigs < ActiveRecord::Migration[5.2]
  def change
    create_table :barbeque_retry_configs, options: 'ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET=utf8mb4' do |t|
      t.integer :job_definition_id, null: false
      t.integer :retry_limit, null: false, default: 3
      t.float :base_delay, null: false, default: '0.3'
      t.integer :max_delay
      t.boolean :jitter, null: false, default: true
      t.timestamps

      t.index [:job_definition_id], unique: true
    end
  end
end


================================================
FILE: db/migrate/20190311034445_add_notify_failure_only_if_retry_limit_reached_to_barbeque_slack_notifications.rb
================================================
class AddNotifyFailureOnlyIfRetryLimitReachedToBarbequeSlackNotifications < ActiveRecord::Migration[5.2]
  def change
    add_column :barbeque_slack_notifications, :notify_failure_only_if_retry_limit_reached, :boolean, default: false, null: false
  end
end


================================================
FILE: db/migrate/20190315052951_change_barbeque_retry_configs_base_delay_default_value.rb
================================================
class ChangeBarbequeRetryConfigsBaseDelayDefaultValue < ActiveRecord::Migration[5.2]
  def up
    change_column_default :barbeque_retry_configs, :base_delay, 15
  end

  def down
    change_column_default :barbeque_retry_configs, :base_delay, 0.3
  end
end


================================================
FILE: db/migrate/20191029105530_change_job_name_to_case_sensitive.rb
================================================
class ChangeJobNameToCaseSensitive < ActiveRecord::Migration[5.2]
  def up
    change_column :barbeque_job_definitions, :job, :string, collation: 'utf8mb4_bin'
  end

  def down
    change_column :barbeque_job_definitions, :job, :string, collation: nil
  end
end


================================================
FILE: db/migrate/20240415080757_fix_collations.rb
================================================
# In MySQL 8.0, the default collation for utf8mb4 is changed to utf8mb4_0900_ai_ci.
# The column collations are utf8mb4_0900_ai_ci after we ran migrations prior to this
# on MySQL 8.0 because we did not specify collation in create_table (thus the default is used).
# This happens on testing/development but we want to keep utf8mb4_general_ci.
#
# Note that running this should not affect the actual schema and data
# unless you've ran prior migrations on MySQL 8.0.
class FixCollations < ActiveRecord::Migration[6.1]
  def change
    reversible do |direction|
      direction.up do
        # alter column collations and default collation of every tables defined to utf8mb4_general_ci
        execute 'ALTER TABLE barbeque_apps CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_docker_containers CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_ecs_hako_tasks CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_job_executions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_job_queues CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_job_retries CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_retry_configs CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_slack_notifications CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        execute 'ALTER TABLE barbeque_sns_subscriptions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'

        # barbeque_job_definitions contains a column with explicitly specified collation
        execute 'ALTER TABLE barbeque_job_definitions DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci'
        change_column :barbeque_job_definitions, :command, :string, collation: 'utf8mb4_general_ci'
        change_column :barbeque_job_definitions, :description, :text, collation: 'utf8mb4_general_ci'
      end

      direction.down do
        raise ActiveRecord::IrreversibleMigration
      end
    end
  end
end


================================================
FILE: doc/api/job_executions.md
================================================
## GET /v1/job_executions/:message_id
Shows a status of a job_execution.

### Example

#### Request
```
GET /v1/job_executions/147e070d-41a9-4ace-a122-2aa579e18f08 HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 81
Content-Type: application/json; charset=utf-8
ETag: W/"7e91b6b9d378eb1b93717767ccdccc68"
X-Request-Id: 19b16855-477c-4f8a-99a6-db203a472bfe
X-Runtime: 0.008698

{
  "message_id": "147e070d-41a9-4ace-a122-2aa579e18f08",
  "status": "success",
  "id": 257
}
```

## GET /v1/job_executions/:message_id
Shows url to job_execution.

### Example

#### Request
```
GET /v1/job_executions/060a38f2-97a8-42ea-8250-b0c66b8dad77?fields=__default__,html_url HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 163
Content-Type: application/json; charset=utf-8
ETag: W/"dfeb94a1453a12315b86db92ddf3fe01"
X-Request-Id: 8a7713b2-305f-4daf-b877-dc288147915c
X-Runtime: 0.002219

{
  "message_id": "060a38f2-97a8-42ea-8250-b0c66b8dad77",
  "status": "success",
  "id": 258,
  "html_url": "https://barbeque/job_executions/060a38f2-97a8-42ea-8250-b0c66b8dad77"
}
```

## GET /v1/job_executions/:message_id
Returns message of the job_execution.

### Example

#### Request
```
GET /v1/job_executions/d3052505-355f-44aa-9565-9ac325960f6b?fields=__default__,message HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 111
Content-Type: application/json; charset=utf-8
ETag: W/"06553c660e7287f2b1e5d29f01b494f7"
X-Request-Id: e8c5596a-ec4a-407c-9a8d-864d23a32044
X-Runtime: 0.001963

{
  "message_id": "d3052505-355f-44aa-9565-9ac325960f6b",
  "status": "success",
  "id": 259,
  "message": {
    "recipe_id": 12345
  }
}
```

## GET /v1/job_executions/:message_id
Returns error message.

### Example

#### Request
```
GET /v1/job_executions/4df2cbf1-bfb6-46f8-9412-e6adc2349bbe HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 503
Cache-Control: no-cache
Content-Length: 237
Content-Type: application/json; charset=utf-8
X-Request-Id: 78d64fec-e4b5-4a93-8a00-e7201ba7b3ce
X-Runtime: 0.002017

{
  "message": "Mysql2::Error::ConnectionError: Can't connect to MySQL server: SELECT  `barbeque_job_executions`.* FROM `barbeque_job_executions` WHERE `barbeque_job_executions`.`message_id` = '4df2cbf1-bfb6-46f8-9412-e6adc2349bbe' LIMIT 1"
}
```

## POST /v2/job_executions
Enqueues a job execution.

### Parameters
* `application` string (required) - Application name of the job
* `job` string (required) - Class of Job to be enqueued
* `queue` string (required) - Queue name to enqueue a job
* `message` any (required) - Free-format JSON
* `delay_seconds` integer - Set message timer of SQS

### Example

#### Request
```
POST /v2/job_executions HTTP/1.1
Accept: application/json
Content-Length: 89
Content-Type: application/json
Host: www.example.com

{
  "application": "blog",
  "job": "NotifyAuthor",
  "queue": "queue-104",
  "message": {
    "recipe_id": 1
  }
}
```

#### Response
```
HTTP/1.1 201
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 82
Content-Type: application/json; charset=utf-8
ETag: W/"906cbe627aa59752a3654cdc059b930a"
X-Request-Id: ece6706b-cd9b-4d24-9093-7d5bb248bd35
X-Runtime: 0.001741

{
  "message_id": "6dc3cbba-77fe-4922-bd9f-273def490f72",
  "status": "pending",
  "id": null
}
```

## POST /v2/job_executions
Enqueues a job execution with delay_seconds.

### Parameters
* `application` string (required) - Application name of the job
* `job` string (required) - Class of Job to be enqueued
* `queue` string (required) - Queue name to enqueue a job
* `message` any (required) - Free-format JSON
* `delay_seconds` integer - Set message timer of SQS

### Example

#### Request
```
POST /v2/job_executions HTTP/1.1
Accept: application/json
Content-Length: 109
Content-Type: application/json
Host: www.example.com

{
  "application": "blog",
  "job": "NotifyAuthor",
  "queue": "queue-107",
  "message": {
    "recipe_id": 1
  },
  "delay_seconds": 300
}
```

#### Response
```
HTTP/1.1 201
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 82
Content-Type: application/json; charset=utf-8
ETag: W/"2584b3edd06144ea19b89616b028d0e0"
X-Request-Id: 1f9c99b9-8540-4897-9bfc-fc1f6dec2260
X-Runtime: 0.005125

{
  "message_id": "b1413290-1b07-4e0d-b3df-817204c14daa",
  "status": "pending",
  "id": null
}
```


================================================
FILE: doc/api/job_retries.md
================================================
## POST /v1/job_executions/:job_execution_message_id/retries
Enqueues a message to retry a specified message.

### Parameters
* `delay_seconds` integer (only: `0..900`)

### Example

#### Request
```
POST /v1/job_executions/8b861a99-a45b-4720-851a-805c1bc70287/retries HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 201
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 72
Content-Type: application/json; charset=utf-8
ETag: W/"e03d4f08be4c8b4dfd18839dd48708e8"
X-Request-Id: 9c2a3b6f-3888-47da-bc2a-d75137af73bc
X-Runtime: 0.004713

{
  "message_id": "af18541e-9668-4eb2-ac5b-1899dbbe7e1b",
  "status": "pending"
}
```


================================================
FILE: doc/api/revision_locks.md
================================================
## POST /v1/apps/:app_id/revision_lock
Updates a tag of docker_image.

### Parameters
* `revision` string (required) - Docker image revision to lock

### Example

#### Request
```
POST /v1/apps/app-107/revision_lock HTTP/1.1
Accept: application/json
Content-Length: 55
Content-Type: application/json
Host: www.example.com

{
  "revision": "798926db1e623cd51245b70b1f1acb40d780ddc1"
}
```

#### Response
```
HTTP/1.1 201
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 55
Content-Type: application/json; charset=utf-8
ETag: W/"a3f0b3d8c32ee1e318357b5276e50a3c"
X-Request-Id: 0a4f9e4b-d80e-4788-ad77-666152308941
X-Runtime: 0.008506

{
  "revision": "798926db1e623cd51245b70b1f1acb40d780ddc1"
}
```

## DELETE /v1/apps/:app_id/revision_lock
Updates a tag of docker_image.

### Example

#### Request
```
DELETE /v1/apps/app-109/revision_lock HTTP/1.1
Accept: application/json
Content-Length: 0
Content-Type: application/json
Host: www.example.com
```

#### Response
```
HTTP/1.1 204
Cache-Control: no-cache
X-Request-Id: 29434e92-0539-40cc-b38d-011e4d0c6347
X-Runtime: 0.006990
```


================================================
FILE: doc/toc.md
================================================
## Table of Contents
* [api/job_executions.md](api/job_executions.md)
 * [GET /v1/job_executions/:message_id](api/job_executions.md#get-v1job_executionsmessage_id)
 * [GET /v1/job_executions/:message_id](api/job_executions.md#get-v1job_executionsmessage_id-1)
 * [GET /v1/job_executions/:message_id](api/job_executions.md#get-v1job_executionsmessage_id-2)
 * [GET /v1/job_executions/:message_id](api/job_executions.md#get-v1job_executionsmessage_id-3)
 * [POST /v2/job_executions](api/job_executions.md#post-v2job_executions)
 * [POST /v2/job_executions](api/job_executions.md#post-v2job_executions-1)
* [api/job_retries.md](api/job_retries.md)
 * [POST /v1/job_executions/:job_execution_message_id/retries](api/job_retries.md#post-v1job_executionsjob_execution_message_idretries)
* [api/revision_locks.md](api/revision_locks.md)
 * [POST /v1/apps/:app_id/revision_lock](api/revision_locks.md#post-v1appsapp_idrevision_lock)
 * [DELETE /v1/apps/:app_id/revision_lock](api/revision_locks.md#delete-v1appsapp_idrevision_lock)


================================================
FILE: lib/barbeque/config.rb
================================================
require 'erb'
require 'yaml'

module Barbeque
  class Config
    attr_accessor :exception_handler, :executor, :executor_options, :sqs_receive_message_wait_time, :maximum_concurrent_executions, :runner_wait_seconds

    def initialize(options = {})
      options.each do |key, value|
        if respond_to?("#{key}=")
          public_send("#{key}=", value)
        else
          raise KeyError.new("Unexpected option '#{key}' was specified.")
        end
      end
      executor_options.symbolize_keys!
    end
  end

  module ConfigBuilder
    DEFAULT_CONFIG = {
      'exception_handler' => 'RailsLogger',
      'executor' => 'Docker',
      'executor_options' => {},
      # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#API_CreateQueue_RequestParameters
      'sqs_receive_message_wait_time' => 10,
      # nil means unlimited
      'maximum_concurrent_executions' => nil,
      'runner_wait_seconds' => 10,
    }

    def config
      @config ||= build_config
    end

    def build_config(config_name = 'barbeque')
      Config.new(DEFAULT_CONFIG.merge(Rails.application.config_for(config_name)))
    end
  end

  extend ConfigBuilder
end


================================================
FILE: lib/barbeque/docker_image.rb
================================================
module Barbeque
  class DockerImage
    DEFAULT_TAG = 'latest'

    def initialize(str)
      # See: https://github.com/docker/docker/blob/v1.10.2/image/spec/v1.md
      result = str.match(%r{((?<registry>[^/]+)?/)?(?<repository>[\w./-]+)(:(?<tag>[\w.-]+))?\z})
      @repository = result[:repository]
      @tag        = result[:tag] || DEFAULT_TAG
      @registry   = result[:registry] || ENV['BARBEQUE_DOCKER_REGISTRY']
    end

    attr_reader :registry, :repository
    attr_accessor :tag

    def to_s
      [registry, "#{repository}:#{tag}"].compact.join('/')
    end
  end
end


================================================
FILE: lib/barbeque/engine.rb
================================================
# Gems used by Barbeque::Engine
require 'rinku'

module Barbeque
  class Engine < ::Rails::Engine
    isolate_namespace Barbeque

    config.before_configuration do
      # Gems used by Barbeque::Engine, which also have Railtie or Mountable::Engine.
      # Railtie and Mountable::Engine aren't executed when required normally.
      require 'hamlit'
      require 'jquery-rails'
      require 'kaminari'
      require 'sprockets/railtie'
      require 'weak_parameters'
    end
  end
end


================================================
FILE: lib/barbeque/exception_handler.rb
================================================
require 'barbeque/config'

module Barbeque
  module ExceptionHandler
    class << self
      delegate :clear_context, :set_message_context, :handle_exception, to: :handler

      private

      def handler
        @handler ||= const_get(Barbeque.config.exception_handler, false).new
      end
    end

    class RailsLogger
      def initialize
        clear_context
      end

      def clear_context
        @message_id = nil
        @message_type = nil
      end

      # @param [String] message_id
      # @param [String, nil] message_type
      def set_message_context(message_id, message_type)
        @message_id = message_id
        @message_type = message_type
      end

      # @param [Exception] e
      def handle_exception(e)
        Rails.logger.error("#{e.inspect}\nmessage_id: #{@message_id}, message_type: #{@message_type}\n#{e.backtrace.join("\n")}")
      end
    end

    class Raven
      def clear_context
        ::Raven::Context.clear!
      end

      # @param [String] message_id
      # @param [String, nil] message_type
      def set_message_context(message_id, message_type)
        ::Raven.tags_context(message_id: message_id, message_type: message_type)
      end

      # @param [Exception] e
      def handle_exception(e)
        ::Raven.capture_exception(e)
      end
    end

    class Sentry
      def initialize
        ::Sentry.get_current_hub.push_scope
      end

      def clear_context
        ::Sentry.get_current_hub.pop_scope
        ::Sentry.get_current_hub.push_scope
      end

      # @param [String] message_id
      # @param [String, nil] message_type
      def set_message_context(message_id, message_type)
        ::Sentry.configure_scope do |scope|
          scope.set_tags(
            message_id: message_id,
            message_type: message_type
          )
        end
      end

      # @param [Exception] e
      def handle_exception(e)
        ::Sentry.capture_exception(e)
      end
    end
  end
end


================================================
FILE: lib/barbeque/execution_log.rb
================================================
require 'aws-sdk-s3'
require 'active_support'
require 'active_support/core_ext'
require 'barbeque/exception_handler'

module Barbeque
  class ExecutionLog
    DEFAULT_S3_BUCKET_NAME = 'barbeque'

    class << self
      delegate :save_message, :save_stdout_and_stderr, :try_save_stdout_and_stderr, :load, to: :new

      def s3_client
        @s3_client ||= Aws::S3::Client.new
      end
    end

    # @param [Barbeque::JobExecution] execution
    # @param [Barbeque::Message::JobExecution] message
    def save_message(execution, message)
      put(execution, 'message.json', message.body.to_json)
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @param [String] stdout
    # @param [String] stderr
    def save_stdout_and_stderr(execution, stdout, stderr)
      put(execution, 'stdout.txt', stdout)
      put(execution, 'stderr.txt', stderr)
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @param [String] stdout
    # @param [String] stderr
    def try_save_stdout_and_stderr(execution, stdout, stderr)
      save_stdout_and_stderr(execution, stdout, stderr)
    rescue Aws::S3::Errors::ServiceError => e
      ExceptionHandler.handle_exception(e)
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @return [Hash] log
    def load(execution:)
      return {} if execution.pending?

      message = get(execution, 'message.json')
      stdout = get(execution, 'stdout.txt')
      stderr = get(execution, 'stderr.txt')
      if message || stdout || stderr
        {
          'message' => message,
          'stdout' => stdout,
          'stderr' => stderr,
        }
      else
        nil
      end
    end

    private

    def s3_bucket_name
      @s3_bucket_name ||= ENV['BARBEQUE_S3_BUCKET_NAME'] || DEFAULT_S3_BUCKET_NAME
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @param [String] filename
    def s3_key_for(execution, filename)
      "#{execution.app.name}/#{execution.job_definition.job}/#{execution.message_id}/#{filename}"
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @param [String] filename
    # @return [String]
    def get(execution, filename)
      s3_object = ExecutionLog.s3_client.get_object(
        bucket: s3_bucket_name,
        key: s3_key_for(execution, filename),
      )
      s3_object.body.read
    rescue Aws::S3::Errors::NoSuchKey
      nil
    end

    # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
    # @param [String] filename
    # @param [String] content
    def put(execution, filename, content)
      ExecutionLog.s3_client.put_object(
        bucket: s3_bucket_name,
        key: s3_key_for(execution, filename),
        body: content,
      )
    end
  end
end


================================================
FILE: lib/barbeque/execution_poller.rb
================================================
require 'barbeque/exception_handler'
require 'barbeque/executor'

module Barbeque
  class ExecutionPoller
    def initialize(job_queue)
      @job_queue      = job_queue
      @stop_requested = false
    end

    def run
      @job_queue.job_executions.running.find_in_batches do |job_executions|
        job_executions.shuffle.each do |job_execution|
          if @stop_requested
            return
          end
          job_execution.with_lock do
            if job_execution.running?
              poll(job_execution)
            end
          end
        end
      end
      sleep 1
    end

    def stop
      @stop_requested = true
    end

    private

    def poll(job_execution)
      Barbeque::ExceptionHandler.set_message_context(job_execution.message_id, nil)
      executor = Executor.create
      executor.poll_execution(job_execution)
    end
  end
end


================================================
FILE: lib/barbeque/executor/docker.rb
================================================
require 'barbeque/docker_image'
require 'barbeque/execution_log'
require 'barbeque/slack_notifier'
require 'open3'

module Barbeque
  module Executor
    class Docker
      class DockerCommandError < StandardError
      end

      def initialize(**)
      end

      # @param [Barbeque::JobExecution] job_execution
      # @param [Hash] envs
      def start_execution(job_execution, envs)
        docker_image = DockerImage.new(job_execution.job_definition.app.docker_image)
        cmd = build_docker_run_command(docker_image, job_execution.job_definition.command, envs)
        stdout, stderr, status = Open3.capture3(*cmd)
        if status.success?
          Barbeque::DockerContainer.create!(message_id: job_execution.message_id, container_id: stdout.chomp)
          job_execution.update!(status: :running)
        else
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_execution, stdout, stderr)
          job_execution.update!(status: :failed, finished_at: Time.zone.now)
          Barbeque::SlackNotifier.notify_job_execution(job_execution)
          job_execution.retry_if_possible!
        end
      end

      # @param [Barbeque::JobRetry] job_retry
      # @param [Hash] envs
      def start_retry(job_retry, envs)
        job_execution = job_retry.job_execution
        docker_image = DockerImage.new(job_execution.job_definition.app.docker_image)
        cmd = build_docker_run_command(docker_image, job_execution.job_definition.command, envs)
        stdout, stderr, status = Open3.capture3(*cmd)
        if status.success?
          Barbeque::DockerContainer.create!(message_id: job_retry.message_id, container_id: stdout.chomp)
          Barbeque::ApplicationRecord.transaction do
            job_execution.update!(status: :retried)
            job_retry.update!(status: :running)
          end
        else
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_retry, stdout, stderr)
          Barbeque::ApplicationRecord.transaction do
            job_retry.update!(status: :failed, finished_at: Time.zone.now)
            job_execution.update!(status: :failed)
          end
          Barbeque::SlackNotifier.notify_job_retry(job_retry)
          job_execution.retry_if_possible!
        end
      end

      # @param [Barbeque::JobExecution] job_execution
      def poll_execution(job_execution)
        container = Barbeque::DockerContainer.find_by!(message_id: job_execution.message_id)
        info = inspect_container(container.container_id)
        if info['State'] && info['State']['Status'] != 'running'
          finished_at = Time.zone.parse(info['State']['FinishedAt'])
          exit_code = info['State']['ExitCode']
          job_execution.update!(status: exit_code == 0 ? :success : :failed, finished_at: finished_at)

          stdout, stderr = get_logs(container.container_id)
          Barbeque::ExecutionLog.save_stdout_and_stderr(job_execution, stdout, stderr)
          Barbeque::SlackNotifier.notify_job_execution(job_execution)
          if exit_code != 0
            job_execution.retry_if_possible!
          end
        end
      end

      # @param [Barbeque::JobRetry] job_retry
      def poll_retry(job_retry)
        container = Barbeque::DockerContainer.find_by!(message_id: job_retry.message_id)
        job_execution = job_retry.job_execution
        info = inspect_container(container.container_id)
        if info['State'] && info['State']['Status'] != 'running'
          finished_at = Time.zone.parse(info['State']['FinishedAt'])
          exit_code = info['State']['ExitCode']
          status = exit_code == 0 ? :success : :failed
          Barbeque::ApplicationRecord.transaction do
            job_retry.update!(status: status, finished_at: finished_at)
            job_execution.update!(status: status)
          end

          stdout, stderr = get_logs(container.container_id)
          Barbeque::ExecutionLog.save_stdout_and_stderr(job_retry, stdout, stderr)
          Barbeque::SlackNotifier.notify_job_retry(job_retry)
          if status == :failed
            job_execution.retry_if_possible!
          end
        end
      end

      private

      # @param [Barbeque::DockerImage] docker_image
      # @param [Array<String>] command
      # @param [Hash] envs
      def build_docker_run_command(docker_image, command, envs)
        ['docker', 'run', '--detach', *env_options(envs), docker_image.to_s, *command]
      end

      def env_options(envs)
        envs.flat_map do |key, value|
          ['--env', "#{key}=#{value}"]
        end
      end

      # @param [String] container_id
      # @return [Hash] container info
      def inspect_container(container_id)
        stdout, stderr, status = Open3.capture3('docker', 'inspect', container_id)
        if status.success?
          begin
            JSON.parse(stdout)[0]
          rescue JSON::ParserError => e
            raise DockerCommandError.new("Unable to parse JSON: #{e.class}: #{e.message}: #{stdout}")
          end
        else
          raise DockerCommandError.new("Unable to inspect Docker container #{container.container_id}: STDOUT: #{stdout}; STDERR: #{stderr}")
        end
      end

      # @param [String] container_id
      # @return [String] stdout
      # @return [String] stderr
      def get_logs(container_id)
        stdout, stderr, status = Open3.capture3('docker', 'logs', container_id)
        if status.success?
          [stdout, stderr]
        else
          raise DockerCommandError.new("Unable to get Docker container logs #{container.container_id}: STDOUT: #{stdout}; STDERR: #{stderr}")
        end
      end
    end
  end
end


================================================
FILE: lib/barbeque/executor/hako.rb
================================================
require 'barbeque/docker_image'
require 'barbeque/execution_log'
require 'barbeque/hako_s3_client'
require 'barbeque/slack_notifier'
require 'open3'

module Barbeque
  module Executor
    class Hako
      class HakoCommandError < StandardError
      end

      attr_reader :hako_s3_client

      # @param [String] hako_dir
      # @param [Hash] hako_env
      # @param [String] definition_dir
      # @param [String] yaml_dir (deprecated: renamed to definition_dir)
      def initialize(hako_dir:, hako_env: {}, yaml_dir: nil, definition_dir: nil, oneshot_notification_prefix:)
        @hako_dir = hako_dir
        @hako_env = hako_env.stringify_keys
        @definition_dir =
          if definition_dir
            definition_dir
          elsif yaml_dir
            warn 'yaml_dir option is renamed to definition_dir. Please update config/barbeque.yml'
            yaml_dir
          else
            raise ArgumentError.new('definition_dir is required')
          end
        @hako_s3_client = HakoS3Client.new(oneshot_notification_prefix)
      end

      # @param [Barbeque::JobExecution] job_execution
      # @param [Hash] envs
      def start_execution(job_execution, envs)
        docker_image = DockerImage.new(job_execution.job_definition.app.docker_image)
        cmd = build_hako_oneshot_command(docker_image, job_execution.job_definition.command, envs)
        stdout, stderr, status = Bundler.with_unbundled_env { Open3.capture3(@hako_env, *cmd, chdir: @hako_dir) }
        if status.success?
          cluster, task_arn = extract_task_info(stdout)
          Barbeque::EcsHakoTask.create!(message_id: job_execution.message_id, cluster: cluster, task_arn: task_arn)
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_execution, stdout, stderr)
          job_execution.update!(status: :running)
        else
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_execution, stdout, stderr)
          job_execution.update!(status: :failed, finished_at: Time.zone.now)
          Barbeque::SlackNotifier.notify_job_execution(job_execution)
          job_execution.retry_if_possible!
        end
      end

      # @param [Barbeque::JobRetry] job_retry
      # @param [Hash] envs
      def start_retry(job_retry, envs)
        job_execution = job_retry.job_execution
        docker_image = DockerImage.new(job_execution.job_definition.app.docker_image)
        cmd = build_hako_oneshot_command(docker_image, job_execution.job_definition.command, envs)
        stdout, stderr, status = Bundler.with_unbundled_env { Open3.capture3(@hako_env, *cmd, chdir: @hako_dir) }
        if status.success?
          cluster, task_arn = extract_task_info(stdout)
          Barbeque::EcsHakoTask.create!(message_id: job_retry.message_id, cluster: cluster, task_arn: task_arn)
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_retry, stdout, stderr)
          Barbeque::ApplicationRecord.transaction do
            job_execution.update!(status: :retried)
            job_retry.update!(status: :running)
          end
        else
          Barbeque::ExecutionLog.try_save_stdout_and_stderr(job_retry, stdout, stderr)
          Barbeque::ApplicationRecord.transaction do
            job_retry.update!(status: :failed, finished_at: Time.zone.now)
            job_execution.update!(status: :failed)
          end
          Barbeque::SlackNotifier.notify_job_retry(job_retry)
          job_execution.retry_if_possible!
        end
      end

      # @param [Barbeque::JobExecution] job_execution
      def poll_execution(job_execution)
        hako_task = Barbeque::EcsHakoTask.find_by!(message_id: job_execution.message_id)
        task = @hako_s3_client.get_stopped_result(hako_task)
        if task
          status = :failed
          task.containers.each do |container|
            if container.name == 'app'
              status = container.exit_code == 0 ? :success : :failed
            end
          end
          job_execution.update!(status: status, finished_at: task.stopped_at)
          Barbeque::SlackNotifier.notify_job_execution(job_execution)
          if status == :failed
            job_execution.retry_if_possible!
          end
        end
      end

      # @param [Barbeque::JobRetry] job_execution
      def poll_retry(job_retry)
        hako_task = Barbeque::EcsHakoTask.find_by!(message_id: job_retry.message_id)
        job_execution = job_retry.job_execution
        task = @hako_s3_client.get_stopped_result(hako_task)
        if task
          status = :failed
          task.containers.each do |container|
            if container.name == 'app'
              status = container.exit_code == 0 ? :success : :failed
            end
          end
          Barbeque::ApplicationRecord.transaction do
            job_retry.update!(status: status, finished_at: task.stopped_at)
            job_execution.update!(status: status)
          end
          Barbeque::SlackNotifier.notify_job_retry(job_retry)
          if status == :failed
            job_execution.retry_if_possible!
          end
        end
      end

      private

      def build_hako_oneshot_command(docker_image, command, envs)
        jsonnet_path = File.join(@definition_dir, "#{docker_image.repository}.jsonnet")
        yaml_path = File.join(@definition_dir, "#{docker_image.repository}.yml")

        cmd = ['bundle', 'exec', 'hako', 'oneshot', '--no-wait', '--tag', docker_image.tag, *env_options(envs)]
        if File.readable?(jsonnet_path)
          cmd << jsonnet_path
        elsif File.readable?(yaml_path)
          cmd << yaml_path
        else
          raise HakoCommandError.new("No definition found matching '#{docker_image.repository}' in #{@definition_dir}")
        end
        cmd << '--'
        cmd.concat(command)
      end

      def env_options(envs)
        envs.map do |key, value|
          "--env=#{key}=#{value}"
        end
      end

      def extract_task_info(stdout)
        last_line = stdout.lines.last
        if last_line
          begin
            task_info = JSON.parse(last_line)
            cluster = task_info['cluster']
            task_arn = task_info['task_arn']
            if cluster && task_arn
              [cluster, task_arn]
            else
              raise HakoCommandError.new("Unable find cluster and task_arn in JSON: #{stdout}")
            end
          rescue JSON::ParserError => e
            raise HakoCommandError.new("Unable parse the last line as JSON: #{stdout}")
          end
        else
          raise HakoCommandError.new('stdout is empty')
        end
      end
    end
  end
end


================================================
FILE: lib/barbeque/executor.rb
================================================
require 'barbeque/config'
require 'barbeque/executor/docker'
require 'barbeque/executor/hako'

module Barbeque
  # Executor is responsible for starting executions and getting status of
  # executions.
  # Each executor must implement these methods.
  # - #initialize(options)
  #   - Create a executor with executor_options specified in config/barbeque.yml.
  # - #start_execution(job_execution, envs)
  #   - Start execution with environment variables. An executor must update the
  #     execution status from pending.
  # - #poll_execution(job_execution)
  #   - Get the execution status and update the job_execution columns.
  # - #start_retry(job_retry, envs)
  #   - Start retry with environment variables. An executor must update the
  #     retry status from pending and the corresponding execution status.
  # - #poll_retry(job_retry)
  #   - Get the execution status and update the job_retry and job_execution
  #     columns.

  module Executor
    def self.create
      klass = const_get(Barbeque.config.executor, false)
      klass.new(**Barbeque.config.executor_options)
    end
  end
end


================================================
FILE: lib/barbeque/hako_s3_client.rb
================================================
require 'uri'
require 'aws-sdk-ecs'
require 'aws-sdk-s3'

module Barbeque
  class HakoS3Client
    # @param [String] oneshot_notification_prefix S3 location for oneshot notification
    def initialize(oneshot_notification_prefix)
      uri = URI.parse(oneshot_notification_prefix)
      @s3_bucket = uri.host
      @s3_prefix = uri.path.sub(%r{\A/}, '')
      @s3_region = URI.decode_www_form(uri.query || '').to_h['region']
    end

    # @param [Barbeque::EcsHakoTask] hako_task
    # @return [String]
    def s3_key_for_stopped_result(hako_task)
      "#{@s3_prefix}/#{hako_task.task_arn}/stopped.json"
    end

    # @return [Aws::S3::Client]
    def s3_client
      @s3_client ||= Aws::S3::Client.new(region: @s3_region, http_read_timeout: 5)
    end

    # @param [Barbeque::EcsHakoTask] hako_task
    # @return [Aws::ECS::Types::Task, nil]
    def get_stopped_result(hako_task)
      object = s3_client.get_object(bucket: @s3_bucket, key: s3_key_for_stopped_result(hako_task))
      result = JSON.parse(object.body.read)
      detail = result.fetch('detail')
      Aws::Json::Parser.new(Aws::ECS::Client.api.operation('describe_tasks').output.shape.member(:tasks).shape.member).parse(JSON.dump(detail))
    rescue Aws::S3::Errors::NoSuchKey
      nil
    end
  end
end


================================================
FILE: lib/barbeque/maintenance.rb
================================================
module Barbeque
  module Maintenance
    def self.database_maintenance_mode?
      ENV['BARBEQUE_DATABASE_MAINTENANCE'] == '1' && ENV['AWS_REGION'].present? && ENV['AWS_ACCOUNT_ID'].present?
    end
  end
end


================================================
FILE: lib/barbeque/message/base.rb
================================================
module Barbeque
  module Message
    # A model wrapping Aws::SQS::Types::Message.
    class Base
      attr_reader :id             # [String] Barbeque::JobExecution is associated via `message_id`
      attr_reader :receipt_handle # [String] Used to ack a message
      attr_reader :type           # [String] "JobExecution", "JobRetry", etc
      attr_reader :sent_timestamp # [String] The time the message was sent to the queue (epoch time in milliseconds)

      # @param [Aws::SQS::Types::Message] raw_message
      # @param message_body [Hash] parse result of `raw_message.body`
      def initialize(raw_message, message_body)
        assign_body(message_body)
        @id             = raw_message.message_id
        @receipt_handle = raw_message.receipt_handle
        @sent_timestamp = raw_message.attributes['SentTimestamp']
      end

      # To distinguish with `Barbeque::Message::InvalidMessage`
      def valid?
        true
      end

      private

      def assign_body(message_body)
        @type = message_body['Type']
      end
    end
  end
end


================================================
FILE: lib/barbeque/message/invalid_message.rb
================================================
require 'barbeque/message/base'

module Barbeque
  module Message
    class InvalidMessage < Base
      def valid?
        false
      end
    end
  end
end


================================================
FILE: lib/barbeque/message/job_execution.rb
================================================
require 'barbeque/message/base'

module Barbeque
  module Message
    class JobExecution < Base
      attr_reader :body        # [Object] free-format JSON
      attr_reader :application # [String] To specify `job_definitions.app.name`
      attr_reader :job         # [String] To specify `job_definitions.job`

      private

      def assign_body(message_body)
        super
        @application = message_body['Application']
        @job  = message_body['Job']
        @body = message_body['Message']
      end
    end
  end
end


================================================
FILE: lib/barbeque/message/job_retry.rb
================================================
require 'barbeque/message/base'

module Barbeque
  module Message
    class JobRetry < Base
      attr_reader :retry_message_id # [String] JobExection's message_id

      private

      def assign_body(message_body)
        super
        @retry_message_id = message_body['RetryMessageId']
      end
    end
  end
end


================================================
FILE: lib/barbeque/message/notification.rb
================================================
require 'barbeque/message/base'

module Barbeque
  module Message
    class Notification < Base
      attr_reader :body        # [Object] free-format JSON
      attr_reader :topic_arn   # [String] To specify `subscription.topic_arn`
      attr_reader :application # [String] To specify `job_definitions.app.name`
      attr_reader :job         # [String] To specify `job_definitions.job`

      # @param [Barneque::SnsSubscription] subscription
      # @return [Barbeque::Message::Notification]
      def set_params_from_subscription(subscription)
        @application = subscription.app.name
        @job = subscription.job_definition.job
        self
      end

      private

      def assign_body(message_body)
        super
        @topic_arn = message_body['TopicArn']
        @body = JSON.parse(message_body['Message'])
      end
    end
  end
end


================================================
FILE: lib/barbeque/message.rb
================================================
require 'barbeque/exception_handler'
require 'barbeque/message/base'
require 'barbeque/message/invalid_message'
require 'barbeque/message/job_execution'
require 'barbeque/message/job_retry'
require 'barbeque/message/notification'

module Barbeque
  module Message
    class << self
      # @param [Aws::SQS::Types::Message] raw_message
      # @return [Barbeque::Message::Base]
      def parse(raw_message)
        body = JSON.parse(raw_message.body)
        klass = find_class(body['Type'])
        klass.new(raw_message, body)
      rescue JSON::ParserError => e
        ExceptionHandler.handle_exception(e)
        InvalidMessage.new(raw_message, {})
      end

      private

      def find_class(type)
        Message.const_get(type, false)
      rescue NameError => e
        ExceptionHandler.handle_exception(e)
        InvalidMessage
      end
    end
  end
end


================================================
FILE: lib/barbeque/message_handler/job_execution.rb
================================================
require 'barbeque/execution_log'
require 'barbeque/executor'
require 'barbeque/slack_notifier'

module Barbeque
  module MessageHandler
    class JobExecution
      # @param [Barbeque::Message::JobExecution] message
      # @param [Barbeque::MessageQueue] message_queue
      def initialize(message:, message_queue:)
        @message = message
        @message_queue = message_queue
      end

      def run
        job_execution = create_job_execution

        begin
          Executor.create.start_execution(job_execution, job_envs)
        rescue Exception => e
          job_execution.update!(status: :error, finished_at: Time.now)
          Barbeque::ExecutionLog.save_stdout_and_stderr(job_execution, '', "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
          Barbeque::SlackNotifier.notify_job_execution(job_execution)
          raise e
        end
      end

      private

      def job_envs
        {
          'BARBEQUE_JOB'         => @message.job,
          'BARBEQUE_MESSAGE'     => @message.body.to_json,
          'BARBEQUE_MESSAGE_ID'  => @message.id,
          'BARBEQUE_QUEUE_NAME'  => @message_queue.job_queue.name,
          'BARBEQUE_RETRY_COUNT' => '0',
          'BARBEQUE_SENT_TIMESTAMP' => @message.sent_timestamp,
        }
      end

      def job_definition
        @job_definition ||= Barbeque::JobDefinition.joins(:app).find_by!(
          job: @message.job,
          barbeque_apps: { name: @message.application },
        )
      end

      def create_job_execution
        Barbeque::JobExecution.transaction do
          Barbeque::JobExecution.create(message_id: @message.id, job_definition: job_definition, job_queue: @message_queue.job_queue).tap do |job_execution|
            Barbeque::ExecutionLog.save_message(job_execution, @message)
            @message_queue.delete_message(@message)
          end
        end
      rescue ActiveRecord::RecordNotUnique => e
        # There's a case where Barbeque receives message which was already received or deleted.
        # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html
        @message_queue.delete_message(@message)
        raise DuplicatedExecution.new(e.message)
      end
    end
  end
end


================================================
FILE: lib/barbeque/message_handler/job_retry.rb
================================================
require 'barbeque/execution_log'
require 'barbeque/executor'
require 'barbeque/slack_notifier'

module Barbeque
  module MessageHandler
    class MessageNotFound < StandardError; end

    class JobRetry
      # @param [Barbeque::Message::JobExecution] message
      # @param [Barbeque::MessageQueue] message_queue
      def initialize(message:, message_queue:)
        @message = message
        @message_queue = message_queue
      end

      def run
        job_retry = create_job_retry

        begin
          Executor.create.start_retry(job_retry, job_envs)
        rescue Exception => e
          job_retry.update!(status: :error, finished_at: Time.now)
          job_execution.update!(status: :error)
          Barbeque::ExecutionLog.save_stdout_and_stderr(job_retry, '', "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
          Barbeque::SlackNotifier.notify_job_retry(job_retry)
          raise e
        end
      end

      private

      def job_execution
        @job_execution ||= Barbeque::JobExecution.find_by!(message_id: @message.retry_message_id)
      end

      def job_envs
        if job_execution.execution_log.nil?
          raise MessageNotFound.new('failed to fetch retried message')
        end

        {
          'BARBEQUE_JOB'         => job_execution.job_definition.job,
          'BARBEQUE_MESSAGE'     => job_execution.execution_log['message'],
          'BARBEQUE_MESSAGE_ID'  => @message.retry_message_id,
          'BARBEQUE_QUEUE_NAME'  => @message_queue.job_queue.name,
          'BARBEQUE_RETRY_COUNT' => job_execution.job_retries.count.to_s,
          'BARBEQUE_SENT_TIMESTAMP' => @message.sent_timestamp,
        }
      end

      def create_job_retry
        Barbeque::JobRetry.transaction do
          Barbeque::JobRetry.create(message_id: @message.id, job_execution: job_execution).tap do
            @message_queue.delete_message(@message)
          end
        end
      rescue ActiveRecord::RecordNotUnique => e
        raise DuplicatedExecution.new(e.message)
      end
    end
  end
end


================================================
FILE: lib/barbeque/message_handler/notification.rb
================================================
require 'barbeque/message_handler/job_execution'

module Barbeque
  module MessageHandler
    class Notification < JobExecution
      # @param [Barbeque::Message::Notification] message
      # @param [Barbeque::MessageQueue] message_queue
      def initialize(message:, message_queue:)
        @message = message
        @message_queue = message_queue

        subscription = SnsSubscription.find_by!(topic_arn: @message.topic_arn, job_queue_id: @message_queue.job_queue.id)
        @message.set_params_from_subscription(subscription)
      end
    end
  end
end


================================================
FILE: lib/barbeque/message_handler.rb
================================================
module Barbeque
  module MessageHandler
    class DuplicatedExecution < StandardError; end
  end
end

require 'barbeque/message_handler/job_execution'
require 'barbeque/message_handler/job_retry'
require 'barbeque/message_handler/notification'


================================================
FILE: lib/barbeque/message_queue.rb
================================================
require 'aws-sdk-sqs'
require 'barbeque/config'
require 'barbeque/message'

module Barbeque
  class MessageQueue
    attr_reader :job_queue

    def initialize(job_queue)
      @job_queue = job_queue
      @messages  = []
      @stop      = false
    end

    # Receive a message from SQS queue.
    # @return [Barbeque::Message::Base]
    def dequeue
      loop do
        return nil if @stop
        message = receive_message
        if message
          if message.valid?
            return message
          else
            delete_message(message)
          end
        end
      end
    end

    # Remove a message from SQS queue.
    # @param [Barbeque::Message::Base] message
    def delete_message(message)
      client.delete_message(
        queue_url: @job_queue.queue_url,
        receipt_handle: message.receipt_handle,
      )
    end

    def stop!
      @stop = true
    end

    private

    def receive_message
      result = client.receive_message(
        queue_url: @job_queue.queue_url,
        wait_time_seconds: Barbeque.config.sqs_receive_message_wait_time,
        max_number_of_messages: 1,
        attribute_names: ['SentTimestamp'],
      )
      if result.messages[0]
        Barbeque::Message.parse(result.messages[0])
      end
    end

    def client
      @client ||= Aws::SQS::Client.new
    end
  end
end


================================================
FILE: lib/barbeque/retry_poller.rb
================================================
require 'barbeque/exception_handler'
require 'barbeque/executor'

module Barbeque
  class RetryPoller
    def initialize(job_queue)
      @job_queue      = job_queue
      @stop_requested = false
    end

    def run
      Barbeque::JobRetry
        .joins(:job_execution)
        .running
        .merge(Barbeque::JobExecution.where(job_queue: @job_queue))
        .find_in_batches do |job_retries|
        job_retries.shuffle.each do |job_retry|
          if @stop_requested
            return
          end
          job_retry.with_lock do
            if job_retry.running?
              poll(job_retry)
            end
          end
        end
      end
      sleep 1
    end

    def stop
      @stop_requested = true
    end

    private

    def poll(job_retry)
      Barbeque::ExceptionHandler.set_message_context(job_retry.message_id, nil)
      executor = Executor.create
      executor.poll_retry(job_retry)
    end
  end
end


================================================
FILE: lib/barbeque/runner.rb
================================================
require 'barbeque/config'
require 'barbeque/exception_handler'
require 'barbeque/execution_log'
require 'barbeque/message_handler'
require 'barbeque/message_queue'

module Barbeque
  # Part of barbeque-worker.
  # Runner dequeues a message from {MessageQueue} (Amazon SQS) and dispatches
  # it to message handler.

  class Runner
    def initialize(job_queue)
      @job_queue = job_queue
    end

    def run
      keep_maximum_concurrent_executions

      message = message_queue.dequeue
      return unless message

      Barbeque::ExceptionHandler.set_message_context(message.id, message.type)
      handler = MessageHandler.const_get(message.type, false)
      handler.new(message: message, message_queue: message_queue).run
    end

    def stop
      message_queue.stop!
    end

    private

    def message_queue
      @message_queue ||= MessageQueue.new(@job_queue)
    end

    def keep_maximum_concurrent_executions
      max_num = Barbeque.config.maximum_concurrent_executions
      unless max_num
        # nil means unlimited
        return
      end

      loop do
        current_num = @job_queue.job_executions.where(status: :running).count + Barbeque::JobRetry.where(status: :running).joins(job_execution: :job_queue).merge(Barbeque::JobQueue.where(id: @job_queue.id)).count
        if current_num < max_num
          return
        end
        interval = Barbeque.config.runner_wait_seconds
        Rails.logger.info("#{current_num} executions are running but maximum_concurrent_executions is configured to #{max_num}. Waiting #{interval} seconds...")
        sleep(interval)
      end
    end
  end
end


================================================
FILE: lib/barbeque/slack_client.rb
================================================
require 'net/http'
require 'json'
require 'uri'

module Barbeque
  class SlackClient
    def initialize(channel)
      @channel = channel
    end

    def notify_success(message)
      post_slack(
        attachments: [{
          text: message,
          color: 'good',
          mrkdwn_in: ['text'],
        }],
      )
    end

    def notify_failure(message)
      post_slack(
        attachments: [{
          text: message,
          color: 'danger',
          mrkdwn_in: ['text'],
        }],
      )
    end

    private

    def post_slack(payload)
      Net::HTTP.post_form(
        endpoint_uri,
        payload: default_payload.merge(payload).to_json,
      )
    end

    def default_payload
      { link_names: 1, channel: @channel }
    end

    def endpoint_uri
      @endpoint_uri ||= URI.parse(ENV['SLACK_WEBHOOK_URL'])
    end
  end
end


================================================
FILE: lib/barbeque/slack_notifier.rb
================================================
require 'barbeque/slack_client'

module Barbeque
  module SlackNotifier
    class << self
      # @param [Barbeque::JobExecution] job_execution
      def notify_job_execution(job_execution)
        return if job_execution.slack_notification.nil?

        client = Barbeque::SlackClient.new(job_execution.slack_notification.channel)
        if job_execution.success?
          if job_execution.slack_notification.notify_success
            client.notify_success("*[SUCCESS]* Succeeded to execute #{job_execution_link(job_execution)}")
          end
        elsif job_execution.failed?
          if should_notify_failure?(job_execution)
            client.notify_failure(
              "*[FAILURE]* Failed to execute #{job_execution_link(job_execution)}" \
              " #{job_execution.slack_notification.failure_notification_text}"
            )
          end
        else
          client.notify_failure(
            "*[ERROR]* Failed to execute #{job_execution_link(job_execution)}" \
            " #{job_execution.slack_notification.failure_notification_text}"
          )
        end
      end

      # @param [Barbeque::JobRetry] job_retry
      def notify_job_retry(job_retry)
        return if job_retry.slack_notification.nil?

        client = Barbeque::SlackClient.new(job_retry.slack_notification.channel)
        if job_retry.success?
          if job_retry.slack_notification.notify_success
            client.notify_success("*[SUCCESS]* Succeeded to retry #{job_retry_link(job_retry)}")
          end
        elsif job_retry.failed?
          if should_notify_failure?(job_retry.job_execution)
            client.notify_failure(
              "*[FAILURE]* Failed to retry #{job_retry_link(job_retry)}" \
              " #{job_retry.slack_notification.failure_notification_text}"
            )
          end
        else
          client.notify_failure(
            "*[ERROR]* Failed to retry #{job_retry_link(job_retry)}" \
            " #{job_retry.slack_notification.failure_notification_text}"
          )
        end
      end

      private

      def barbeque_host
        ENV['BARBEQUE_HOST']
      end

      def job_execution_link(job_execution)
        "<#{job_execution_url(job_execution)}|#{job_execution.job_definition.job} ##{job_execution.id}>"
      end

      def job_execution_url(job_execution)
        Barbeque::Engine.routes.url_helpers.job_execution_url(job_execution, host: barbeque_host)
      end

      def job_retry_link(job_retry)
        "<#{job_retry_url(job_retry)}|#{job_retry.job_definition.job}'s retry ##{job_retry.id}>"
      end

      def job_retry_url(job_retry)
        Barbeque::Engine.routes.url_helpers.job_execution_job_retry_url(job_retry.job_execution, job_retry, host: barbeque_host)
      end

      def should_notify_failure?(job_execution_with_slack_notification)
        unless job_execution_with_slack_notification.slack_notification.notify_failure_only_if_retry_limit_reached
          return true
        end

        unless job_execution_with_slack_notification.job_definition.retry_config
          return true
        end

        job_execution_with_slack_notification.job_definition.retry_config.retry_limit <= job_execution_with_slack_notification.job_retries.count
      end
    end
  end
end


================================================
FILE: lib/barbeque/version.rb
================================================
module Barbeque
  VERSION = '2.10.0'
end


================================================
FILE: lib/barbeque/worker.rb
================================================
require 'barbeque/exception_handler'
require 'barbeque/execution_poller'
require 'barbeque/retry_poller'
require 'barbeque/runner'
require 'serverengine'

module Barbeque
  module Worker
    DEFAULT_QUEUE = ENV['BARBEQUE_DEFAULT_QUEUE'] || 'default'

    class UnexpectedMessageType < StandardError; end

    def self.run(
      worker_type: 'process',
      workers:     4,
      daemonize:   false,
      log:         $stdout,
      log_level:   :info,
      pid_path:    '/tmp/barbeque_worker.pid',
      supervisor:  true
    )
      options = {
        worker_type: worker_type,
        workers:     workers,
        daemonize:   daemonize,
        log:         log,
        log_level:   log_level,
        pid_path:    pid_path,
        supervisor:  supervisor,
      }

      worker = ServerEngine.create(nil, Barbeque::Worker, options)
      worker.run
    end

    def initialize
      queue_name = ENV['BARBEQUE_QUEUE'] || DEFAULT_QUEUE
      queue      = Barbeque::JobQueue.find_by!(name: queue_name)

      @command =
        case worker_id
        when 0
          ExecutionPoller.new(queue)
        when 1
          RetryPoller.new(queue)
        else
          Runner.new(queue)
        end
    end

    def run
      $0 = "barbeque-worker (worker_id=#{worker_id} command=#{@command.class.name})"
      until @stop
        begin
          execute_command
        rescue => e
          Barbeque::ExceptionHandler.handle_exception(e)
        end
        Barbeque::ExceptionHandler.clear_context
      end
    end

    def stop
      @stop = true
      @command.stop
    end

    def execute_command
      @command.run
    end
  end
end


================================================
FILE: lib/barbeque.rb
================================================
require 'barbeque/docker_image'
require 'barbeque/engine'
require 'barbeque/execution_log'


================================================
FILE: lib/tasks/barbeque_tasks.rake
================================================
namespace :barbeque do
  desc 'Start worker to execute jobs'
  task worker: :environment do
    ENV['BARBEQUE_QUEUE'] ||= 'default'
    require 'barbeque/worker'

    Barbeque::Worker.run(
      workers:    (ENV['BARBEQUE_WORKER_NUM'] || 4).to_i,
      daemonize:  ENV['DAEMONIZE_BARBEQUE'] == '1',
      log:        ENV['BARBEQUE_LOG_TO_STDOUT'] == '1' ? $stdout : Rails.root.join('log/barbeque_worker.log').to_s,
      log_level:  Rails.env.production? ? :info : :debug,
      pid_path:   Rails.root.join('tmp/pids/barbeque_worker.pid').to_s,
      supervisor: Rails.env.production?,
    )
  end
end


================================================
FILE: spec/barbeque/config_builder_spec.rb
================================================
require 'rails_helper'
require 'barbeque/config'

describe Barbeque::ConfigBuilder do
  describe '#build_config' do
    it 'returns Barbeque::Config loaded from config/barbeque.yml' do
      config = Barbeque.build_config
      expect(config.executor).to eq('Docker')
    end

    context 'when it has no config' do
      it 'returns default config' do
        config = Barbeque.build_config('barbeque.empty')
        expect(config.executor_options).to eq({})
      end
    end

    context 'given erb' do
      it 'evaluates barbeque.yml as erb' do
        config = Barbeque.build_config('barbeque.erb')
        expect(config.executor).to eq('FooBar')
      end
    end
  end
end


================================================
FILE: spec/barbeque/config_spec.rb
================================================
require 'rails_helper'
require 'barbeque/config'

describe Barbeque::Config do
  describe '#executor_options' do
    it 'returns Hash whose keys are symbolized without symbolizing values' do
      options = { 'foo' => { 'bar' => 'baz' } }
      expect(Barbeque::Config.new(executor_options: options).executor_options).
        to eq({ foo: { 'bar' => 'baz' } })
    end
  end
end


================================================
FILE: spec/barbeque/docker_image_spec.rb
================================================
require 'barbeque'

describe Barbeque::DockerImage do
  it 'parses docker image name without a tag' do
    image = Barbeque::DockerImage.new('cookpad')
    expect(image.repository).to eq('cookpad')
    expect(image.tag).to eq('latest')
  end

  it 'parses docker image name with a tag' do
    image = Barbeque::DockerImage.new('cookpad-ruby:2.2')
    expect(image.repository).to eq('cookpad-ruby')
    expect(image.tag).to eq('2.2')
  end

  it 'parses docker image name with a host' do
    image = Barbeque::DockerImage.new('docker.io/library/ruby')
    expect(image.repository).to eq('library/ruby')
    expect(image.tag).to eq('latest')
    expect(image.registry).to eq('docker.io')
  end

  describe '#to_s' do
    context 'with docker registry specified' do
      let(:image_name) { 'cookpad-ruby:2.2' }
      let(:docker_registry) { 'docker-registry-001:80' }

      around do |example|
        begin
          orig, ENV['BARBEQUE_DOCKER_REGISTRY'] = ENV['BARBEQUE_DOCKER_REGISTRY'], docker_registry
          example.run
        ensure
          ENV['BARBEQUE_DOCKER_REGISTRY'] = orig
        end
      end

      it 'prepends docker registry' do
        image = Barbeque::DockerImage.new(image_name)
        expect(image.to_s).to eq("#{docker_registry}/#{image_name}")
      end
    end
  end
end


================================================
FILE: spec/barbeque/exception_handler_spec.rb
================================================
require 'rails_helper'
require 'barbeque/exception_handler'

describe Barbeque::ExceptionHandler do
  describe '.handle_exception' do
    let(:exception) do
      StandardError.new('something went wrong').tap do |e|
        e.set_backtrace(['barbeque.rb:1'])
      end
    end
    let(:handler) { 'RailsLogger' }

    before do
      allow(Barbeque).to receive_message_chain(:config, :exception_handler).and_return(handler)
    end

    it 'handles exception with configured handler' do
      expect_any_instance_of(Barbeque::ExceptionHandler::RailsLogger).to receive(:handle_exception).with(exception)
      Barbeque::ExceptionHandler.handle_exception(exception)
    end
  end
end


================================================
FILE: spec/barbeque/execution_log_spec.rb
================================================
require 'rails_helper'
require 'barbeque/execution_log'

describe Barbeque::ExecutionLog do
  let(:job_execution) { create(:job_execution, status: status) }
  let(:s3_client) { double('Aws::S3::Client') }
  let(:message) { ['hello'] }
  let(:stdout)  { 'stdout' }
  let(:stderr)  { 'stderr' }

  before do
    allow(Barbeque::ExecutionLog).to receive(:s3_client).and_return(s3_client)
  end

  describe '.fetch' do
    def make_s3_object(content)
      Aws::S3::Types::GetObjectOutput.new(body: StringIO.new(content))
    end

    context 'when job_execution is finished' do
      let(:status) { 'success' }

      before do
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_execution.message_id}/message.json",
          bucket: 'barbeque',
        ).and_return(make_s3_object(message.to_json))
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_execution.message_id}/stdout.txt",
          bucket: 'barbeque',
        ).and_return(make_s3_object(stdout))
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_execution.message_id}/stderr.txt",
          bucket: 'barbeque',
        ).and_return(make_s3_object(stderr))
      end

      it 'fetches log from S3 for the job_execution' do
        expect(Barbeque::ExecutionLog.load(execution: job_execution)).to eq({
          'message' => message.to_json,
          'stdout'  => stdout,
          'stderr'  => stderr,
        })
      end
    end

    context 'when job_execution is pending' do
      let(:status) { 'pending' }

      it 'returns empty hash' do
        expect(Barbeque::ExecutionLog.load(execution: job_execution)).to eq({})
      end
    end

    context 'when job_retry is given' do
      let(:job_retry) { create(:job_retry, job_execution: job_execution) }
      let(:status) { 'success' }

      before do
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_retry.message_id}/message.json",
          bucket: 'barbeque',
          ).and_raise(Aws::S3::Errors::NoSuchKey.new(nil, 'The specified key does not exist.'))
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_retry.message_id}/stdout.txt",
          bucket: 'barbeque',
        ).and_return(make_s3_object(stdout))
        allow(s3_client).to receive(:get_object).with(
          key: "#{job_execution.app.name}/#{job_execution.job_definition.job}/#{job_retry.message_id}/stderr.txt",
          bucket: 'barbeque',
        ).and_return(make_s3_object(stderr))
      end

      it 'fetches log from S3 for the job_retry' do
        expect(Barbeque::ExecutionLog.load(execution: job_retry)).to eq({
          'message' => nil,
          'stdout'  => stdout,
          'stderr'  => stderr,
        })
      end
    end
  end
end


================================================
FILE: spec/barbeque/execution_poller_spec.rb
================================================
require 'rails_helper'
require 'barbeque/execution_poller'

RSpec.describe Barbeque::ExecutionPoller do
  let(:job_queue) { create(:job_queue) }
  let(:execution_poller) { described_class.new(job_queue) }
  let(:executor) { double('Barbeque::Executor::Docker') }

  before do
    allow(Barbeque::Executor::Docker).to receive(:new).and_return(executor)
  end

  describe '#run' do
    context 'when there is a running job execution' do
      let(:job_execution) { create(:job_execution, status: :running, job_queue: job_queue) }

      it 'polls the job execution' do
        expect(executor).to receive(:poll_execution).with(job_execution)
        execution_poller.run
      end
    end
  end
end


================================================
FILE: spec/barbeque/executor/docker_spec.rb
================================================
require 'rails_helper'
require 'barbeque/executor/docker'

RSpec.describe Barbeque::Executor::Docker do
  let(:executor) { described_class.new }
  let(:command) { ['rake', 'test'] }
  let(:job_definition) { FactoryBot.create(:job_definition, command: ['rake', 'test']) }
  let(:job_execution) { FactoryBot.create(:job_execution, job_definition: job_definition, status: :pending) }
  let(:container_id) { '59efea4938e5d11ff6e70441d7442614dc1da014e64a8144c87a7608e27e240c' }
  let(:sqs_client) { instance_double(Aws::SQS::Client) }

  around do |example|
    original = ENV['BARBEQUE_HOST']
    ENV['BARBEQUE_HOST'] = 'https://barbeque'
    example.run
    ENV['BARBEQUE_HOST'] = original
  end

  before do
    allow(Barbeque::MessageRetryingService).to receive(:sqs_client).and_return(sqs_client)
  end

  describe 'job_execution' do
    describe '#start_execution' do
      let(:status) { double('Process::Status', success?: true) }
      let(:stdout) { container_id }
      let(:stderr) { '' }

      it 'starts Docker container' do
        expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
        executor.start_execution(job_execution, {})
        expect(Barbeque::DockerContainer.where(message_id: job_execution.message_id, container_id: container_id)).to be_exist
      end

      it 'sets running status' do
        expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
        expect(job_execution).to be_pending
        executor.start_execution(job_execution, {})
        job_execution.reload
        expect(job_execution).to be_running
      end

      context 'when docker-run fails' do
        before do
          expect(status).to receive(:success?).and_return(false)
        end

        it 'sets failed status' do
          expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
          expect(Barbeque::ExecutionLog).to receive(:try_save_stdout_and_stderr).with(job_execution, stdout, stderr)
          executor.start_execution(job_execution, {})
          job_execution.reload
          expect(job_execution).to be_failed
        end

        context 'with retry_config' do
          before do
            FactoryBot.create(:retry_config, job_definition: job_definition)
          end

          it 'performs retry' do
            expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
            expect(Barbeque::ExecutionLog).to receive(:try_save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(Barbeque::MessageRetryingService.sqs_client).to receive(:send_message).with(queue_url: a_kind_of(String), message_body: a_kind_of(String), delay_seconds: a_kind_of(Integer))
            executor.start_execution(job_execution, {})
            job_execution.reload
            expect(job_execution).to be_retried
          end
        end
      end
    end

    describe '#poll_execution' do
      let(:inspect_status) { double('Process::Status', success?: true) }
      let(:container_info) do
        {
          'State' => {
            'Status' => 'exited',
            'FinishedAt' => '2017-07-11T09:17:32.013951633Z',
            'ExitCode' => 0,
          },
        }
      end
      let(:stdout) { 'stdout' }
      let(:stderr) { 'stderr' }
      let(:log_status) { double('Process::Status', success?: true) }

      before do
        job_execution.update!(status: :running)
        Barbeque::DockerContainer.create!(message_id: job_execution.message_id, container_id: container_id)
        expect(Open3).to receive(:capture3).with('docker', 'inspect', container_id) {
          [JSON.dump([container_info]), '', inspect_status]
        }
      end

      context 'when job succeeds' do
        it 'sets success status' do
          expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
          expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
          expect(job_execution).to be_running
          executor.poll_execution(job_execution)
          job_execution.reload
          expect(job_execution).to be_success
        end

        context 'when successful slack_notification is configured' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: true) }

          before do
            job_execution.job_definition.update!(slack_notification: slack_notification)
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
          end

          it 'sends slack notification' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(slack_client).to receive(:notify_success)
            executor.poll_execution(job_execution)
          end
        end
      end

      context 'when job is running' do
        before do
          container_info['State']['Status'] = 'running'
          container_info['State']['FinishedAt'] = '0001-01-01T00:00:00Z'
        end

        it 'does nothing' do
          expect(job_execution).to be_running
          executor.poll_execution(job_execution)
          job_execution.reload
          expect(job_execution).to be_running
        end
      end

      context 'when job fails' do
        before do
          container_info['State']['ExitCode'] = 1
        end

        it 'sets failed status' do
          expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
          expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
          expect(job_execution).to be_running
          executor.poll_execution(job_execution)
          job_execution.reload
          expect(job_execution).to be_failed
        end

        context 'when slack_notification is configured' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: false) }

          before do
            job_execution.job_definition.update!(slack_notification: slack_notification)
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
          end

          it 'sends slack notification' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(slack_client).to receive(:notify_failure)
            executor.poll_execution(job_execution)
          end
        end

        context 'with retry_config' do
          before do
            FactoryBot.create(:retry_config, job_definition: job_definition)
          end

          it 'performs retry' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(Barbeque::MessageRetryingService.sqs_client).to receive(:send_message).with(queue_url: a_kind_of(String), message_body: a_kind_of(String), delay_seconds: a_kind_of(Integer))
            expect(job_execution).to be_running
            executor.poll_execution(job_execution)
            job_execution.reload
            expect(job_execution).to be_retried
          end
        end

        context 'with retry_config and slack_notification (notify_failure_only_if_retry_limit_reached: false)' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: false) }

          before do
            job_execution.job_definition.update!(slack_notification: slack_notification)
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
            FactoryBot.create(:retry_config, job_definition: job_definition)
          end

          it 'sends slack notification' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(Barbeque::MessageRetryingService.sqs_client).to receive(:send_message).with(queue_url: a_kind_of(String), message_body: a_kind_of(String), delay_seconds: a_kind_of(Integer))
            expect(slack_client).to receive(:notify_failure)
            executor.poll_execution(job_execution)
          end
        end

        context 'with retry_config and slack_notification (notify_failure_only_if_retry_limit_reached: true)' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: false, notify_failure_only_if_retry_limit_reached: true) }

          before do
            job_execution.job_definition.update!(slack_notification: slack_notification)
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
            FactoryBot.create(:retry_config, job_definition: job_definition)
          end

          it 'does not send slack notification' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_execution, stdout, stderr)
            expect(Barbeque::MessageRetryingService.sqs_client).to receive(:send_message).with(queue_url: a_kind_of(String), message_body: a_kind_of(String), delay_seconds: a_kind_of(Integer))
            expect(slack_client).not_to receive(:notify_failure)
            executor.poll_execution(job_execution)
          end
        end
      end
    end
  end

  describe 'job_retry' do
    let(:job_retry) { FactoryBot.create(:job_retry, job_execution: job_execution, status: :pending) }
    let(:status) { double('Process::Status', success?: true) }
    let(:stdout) { container_id }
    let(:stderr) { '' }

    before do
      job_execution.update!(status: :failed)
    end

    describe '#start_retry' do
      it 'starts Docker container' do
        expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
        executor.start_retry(job_retry, {})
        expect(Barbeque::DockerContainer.where(message_id: job_retry.message_id, container_id: container_id)).to be_exist
      end

      it 'sets running status' do
        expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
        expect(job_retry).to be_pending
        expect(job_execution).to be_failed
        executor.start_retry(job_retry, {})
        job_retry.reload
        job_execution.reload
        expect(job_retry).to be_running
        expect(job_execution).to be_retried
      end

      context 'when docker-run fails' do
        before do
          expect(status).to receive(:success?).and_return(false)
        end

        it 'sets failed status' do
          expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
          expect(Barbeque::ExecutionLog).to receive(:try_save_stdout_and_stderr).with(job_retry, stdout, stderr)
          expect(job_retry).to be_pending
          expect(job_execution).to be_failed
          executor.start_retry(job_retry, {})
          job_retry.reload
          job_execution.reload
          expect(job_retry).to be_failed
          expect(job_execution).to be_failed
        end

        context 'with retry_config' do
          before do
            FactoryBot.create(:retry_config, job_definition: job_definition)
          end

          it 'performs retry' do
            expect(Open3).to receive(:capture3).with('docker', 'run', '--detach', job_execution.job_definition.app.docker_image, 'rake', 'test').and_return([stdout, stderr, status])
            expect(Barbeque::ExecutionLog).to receive(:try_save_stdout_and_stderr).with(job_retry, stdout, stderr)
            expect(Barbeque::MessageRetryingService.sqs_client).to receive(:send_message).with(queue_url: a_kind_of(String), message_body: a_kind_of(String), delay_seconds: a_kind_of(Integer))
            expect(job_retry).to be_pending
            expect(job_execution).to be_failed
            executor.start_retry(job_retry, {})
            job_retry.reload
            job_execution.reload
            expect(job_retry).to be_failed
            expect(job_execution).to be_retried
          end
        end
      end
    end

    describe '#poll_retry' do
      let(:inspect_status) { double('Process::Status', success?: true) }
      let(:container_info) do
        {
            'State' => {
              'Status' => 'exited',
              'FinishedAt' => '2017-07-11T09:17:32.013951633Z',
              'ExitCode' => 0,
            },
        }
      end
      let(:stdout) { 'stdout' }
      let(:stderr) { 'stderr' }
      let(:log_status) { double('Process::Status', success?: true) }

      before do
        job_execution.update!(status: :retried)
        job_retry.update!(status: :running)
        Barbeque::DockerContainer.create!(message_id: job_retry.message_id, container_id: container_id)
        expect(Open3).to receive(:capture3).with('docker', 'inspect', container_id) {
          [JSON.dump([container_info]), '', inspect_status]
        }
      end

      context 'when retried job succeeds' do
        it 'sets success status' do
          expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
          expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_retry, stdout, stderr)
          expect(job_retry).to be_running
          expect(job_execution).to be_retried
          executor.poll_retry(job_retry)
          job_retry.reload
          job_execution.reload
          expect(job_retry).to be_success
          expect(job_execution).to be_success
        end

        context 'when successful slack_notification is configured' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: true) }

          before do
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
          end

          it 'sends slack notification' do
            expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
            expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_retry, stdout, stderr)
            job_execution.job_definition.update!(slack_notification: slack_notification)
            expect(slack_client).to receive(:notify_success)
            executor.poll_retry(job_retry)
          end
        end
      end

      context 'when retried job is running' do
        before do
          container_info['State']['Status'] = 'running'
          container_info['State']['FinishedAt'] = '0001-01-01T00:00:00Z'
        end

        it 'does nothing' do
          expect(job_retry).to be_running
          expect(job_execution).to be_retried
          executor.poll_retry(job_retry)
          job_retry.reload
          job_execution.reload
          expect(job_retry).to be_running
          expect(job_execution).to be_retried
        end
      end

      context 'when retried job fails' do
        before do
          container_info['State']['ExitCode'] = 1
        end

        it 'sets failed status' do
          expect(Open3).to receive(:capture3).with('docker', 'logs', container_id).and_return([stdout, stderr, log_status])
          expect(Barbeque::ExecutionLog).to receive(:save_stdout_and_stderr).with(job_retry, stdout, stderr)
          expect(job_retry).to be_running
          expect(job_execution).to be_retried
          executor.poll_retry(job_retry)
          job_retry.reload
          job_execution.reload
          expect(job_retry).to be_failed
          expect(job_execution).to be_failed
        end

        context 'when slack_notification is configured' do
          let(:slack_client) { double('Barbeque::SlackClient') }
          let(:slack_notification) { FactoryBot.create(:slack_notification, notify_success: false) }

          before do
            job_execution.job_definition.update!(slack_notification: slack_notification)
            allow(Barbeque::SlackClient).to receive(:new).with(slack_notification.channel).and_return(slack_client)
          end

          it 'sends slack n
Download .txt
gitextract_3xnxz7z0/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rspec
├── CHANGELOG.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── app/
│   ├── assets/
│   │   ├── config/
│   │   │   └── barbeque_manifest.js
│   │   ├── images/
│   │   │   └── barbeque/
│   │   │       └── .keep
│   │   ├── javascripts/
│   │   │   └── barbeque/
│   │   │       ├── application.js
│   │   │       ├── job_definitions.js
│   │   │       └── job_queues.js
│   │   └── stylesheets/
│   │       └── barbeque/
│   │           ├── application.css
│   │           └── job_definitions.css
│   ├── controllers/
│   │   └── barbeque/
│   │       ├── api/
│   │       │   ├── application_controller.rb
│   │       │   ├── job_executions_controller.rb
│   │       │   ├── job_retries_controller.rb
│   │       │   └── revision_locks_controller.rb
│   │       ├── application_controller.rb
│   │       ├── apps_controller.rb
│   │       ├── job_definitions_controller.rb
│   │       ├── job_executions_controller.rb
│   │       ├── job_queues_controller.rb
│   │       ├── job_retries_controller.rb
│   │       ├── monitors_controller.rb
│   │       └── sns_subscriptions_controller.rb
│   ├── helpers/
│   │   └── barbeque/
│   │       ├── application_helper.rb
│   │       ├── job_definitions_helper.rb
│   │       └── job_executions_helper.rb
│   ├── models/
│   │   ├── barbeque/
│   │   │   ├── api/
│   │   │   │   ├── application_resource.rb
│   │   │   │   ├── database_maintenance_resource.rb
│   │   │   │   ├── job_execution_resource.rb
│   │   │   │   ├── job_retry_resource.rb
│   │   │   │   └── revision_lock_resource.rb
│   │   │   ├── app.rb
│   │   │   ├── application_record.rb
│   │   │   ├── docker_container.rb
│   │   │   ├── ecs_hako_task.rb
│   │   │   ├── job_definition.rb
│   │   │   ├── job_execution.rb
│   │   │   ├── job_queue.rb
│   │   │   ├── job_retry.rb
│   │   │   ├── retry_config.rb
│   │   │   ├── slack_notification.rb
│   │   │   └── sns_subscription.rb
│   │   └── concerns/
│   │       └── .keep
│   ├── services/
│   │   └── barbeque/
│   │       ├── message_enqueuing_service.rb
│   │       ├── message_retrying_service.rb
│   │       └── sns_subscription_service.rb
│   └── views/
│       ├── barbeque/
│       │   ├── apps/
│       │   │   ├── _form.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   └── show.html.haml
│       │   ├── job_definitions/
│       │   │   ├── _form.html.haml
│       │   │   ├── _retry_configuration_field.html.haml
│       │   │   ├── _slack_notification_field.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   ├── show.html.haml
│       │   │   └── stats.html.haml
│       │   ├── job_executions/
│       │   │   └── show.html.haml
│       │   ├── job_queues/
│       │   │   ├── _form.html.haml
│       │   │   ├── edit.html.haml
│       │   │   ├── index.html.haml
│       │   │   ├── new.html.haml
│       │   │   └── show.html.haml
│       │   ├── job_retries/
│       │   │   └── show.html.haml
│       │   ├── monitors/
│       │   │   └── index.html.haml
│       │   └── sns_subscriptions/
│       │       ├── _form.html.haml
│       │       ├── edit.html.haml
│       │       ├── index.html.haml
│       │       ├── new.html.haml
│       │       └── show.html.haml
│       └── layouts/
│           └── barbeque/
│               ├── _header.html.haml
│               ├── _sidebar.html.haml
│               ├── application.html.haml
│               ├── apps.html.haml
│               ├── job_definitions.html.haml
│               ├── job_executions.html.haml
│               ├── job_queues.html.haml
│               ├── job_retries.html.haml
│               ├── monitors.html.haml
│               └── sns_subscriptions.html.haml
├── barbeque.gemspec
├── bin/
│   └── rails
├── compose.yaml
├── config/
│   ├── initializers/
│   │   └── garage.rb
│   └── routes.rb
├── db/
│   └── migrate/
│       ├── 20160217020910_create_job_queues.rb
│       ├── 20160219010912_create_job_executions.rb
│       ├── 20160223060807_create_apps.rb
│       ├── 20160223183348_create_job_definitions.rb
│       ├── 20160225020801_add_job_definition_id_to_job_executions.rb
│       ├── 20160412083604_add_finished_at_to_job_executions.rb
│       ├── 20160415043427_create_slack_notifications.rb
│       ├── 20160509041452_add_job_queue_id_to_job_executions.rb
│       ├── 20160516041710_create_job_retries.rb
│       ├── 20160829023237_prefix_barbeque_to_tables.rb
│       ├── 20170420030157_create_barbeque_sns_subscriptions.rb
│       ├── 20170711085157_create_barbeque_docker_containers.rb
│       ├── 20170712075449_create_barbeque_ecs_hako_tasks.rb
│       ├── 20170724025542_add_index_to_job_execution_status.rb
│       ├── 20180411070937_add_index_to_barbeque_job_executions_created_at.rb
│       ├── 20190221050714_create_barbeque_retry_configs.rb
│       ├── 20190311034445_add_notify_failure_only_if_retry_limit_reached_to_barbeque_slack_notifications.rb
│       ├── 20190315052951_change_barbeque_retry_configs_base_delay_default_value.rb
│       ├── 20191029105530_change_job_name_to_case_sensitive.rb
│       └── 20240415080757_fix_collations.rb
├── doc/
│   ├── api/
│   │   ├── job_executions.md
│   │   ├── job_retries.md
│   │   └── revision_locks.md
│   └── toc.md
├── lib/
│   ├── barbeque/
│   │   ├── config.rb
│   │   ├── docker_image.rb
│   │   ├── engine.rb
│   │   ├── exception_handler.rb
│   │   ├── execution_log.rb
│   │   ├── execution_poller.rb
│   │   ├── executor/
│   │   │   ├── docker.rb
│   │   │   └── hako.rb
│   │   ├── executor.rb
│   │   ├── hako_s3_client.rb
│   │   ├── maintenance.rb
│   │   ├── message/
│   │   │   ├── base.rb
│   │   │   ├── invalid_message.rb
│   │   │   ├── job_execution.rb
│   │   │   ├── job_retry.rb
│   │   │   └── notification.rb
│   │   ├── message.rb
│   │   ├── message_handler/
│   │   │   ├── job_execution.rb
│   │   │   ├── job_retry.rb
│   │   │   └── notification.rb
│   │   ├── message_handler.rb
│   │   ├── message_queue.rb
│   │   ├── retry_poller.rb
│   │   ├── runner.rb
│   │   ├── slack_client.rb
│   │   ├── slack_notifier.rb
│   │   ├── version.rb
│   │   └── worker.rb
│   ├── barbeque.rb
│   └── tasks/
│       └── barbeque_tasks.rake
├── spec/
│   ├── barbeque/
│   │   ├── config_builder_spec.rb
│   │   ├── config_spec.rb
│   │   ├── docker_image_spec.rb
│   │   ├── exception_handler_spec.rb
│   │   ├── execution_log_spec.rb
│   │   ├── execution_poller_spec.rb
│   │   ├── executor/
│   │   │   ├── docker_spec.rb
│   │   │   └── hako_spec.rb
│   │   ├── executor_spec.rb
│   │   ├── message_handler/
│   │   │   ├── job_execution_spec.rb
│   │   │   ├── job_retry_spec.rb
│   │   │   └── notification_spec.rb
│   │   ├── message_queue_spec.rb
│   │   ├── message_spec.rb
│   │   ├── retry_poller_spec.rb
│   │   ├── runner_spec.rb
│   │   └── worker_spec.rb
│   ├── controllers/
│   │   └── barbeque/
│   │       ├── apps_controller_spec.rb
│   │       ├── job_definitions_controller_spec.rb
│   │       ├── job_executions_controller_spec.rb
│   │       ├── job_queues_controller_spec.rb
│   │       ├── job_retries_controller_spec.rb
│   │       └── sns_subscriptions_controller_spec.rb
│   ├── dummy/
│   │   ├── .gitignore
│   │   ├── Rakefile
│   │   ├── app/
│   │   │   ├── assets/
│   │   │   │   ├── config/
│   │   │   │   │   └── manifest.js
│   │   │   │   ├── images/
│   │   │   │   │   └── .keep
│   │   │   │   ├── javascripts/
│   │   │   │   │   ├── application.js
│   │   │   │   │   └── channels/
│   │   │   │   │       └── .keep
│   │   │   │   └── stylesheets/
│   │   │   │       └── application.css
│   │   │   ├── controllers/
│   │   │   │   ├── application_controller.rb
│   │   │   │   └── concerns/
│   │   │   │       └── .keep
│   │   │   ├── helpers/
│   │   │   │   └── application_helper.rb
│   │   │   ├── models/
│   │   │   │   └── concerns/
│   │   │   │       └── .keep
│   │   │   └── views/
│   │   │       └── layouts/
│   │   │           ├── application.html.erb
│   │   │           ├── mailer.html.erb
│   │   │           └── mailer.text.erb
│   │   ├── bin/
│   │   │   ├── bundle
│   │   │   ├── rails
│   │   │   ├── rake
│   │   │   ├── setup
│   │   │   ├── spring
│   │   │   ├── update
│   │   │   └── yarn
│   │   ├── config/
│   │   │   ├── application.rb
│   │   │   ├── barbeque.empty.yml
│   │   │   ├── barbeque.erb.yml
│   │   │   ├── barbeque.hako.yml
│   │   │   ├── barbeque.yml
│   │   │   ├── boot.rb
│   │   │   ├── database.yml
│   │   │   ├── environment.rb
│   │   │   ├── environments/
│   │   │   │   ├── development.rb
│   │   │   │   ├── production.rb
│   │   │   │   └── test.rb
│   │   │   ├── initializers/
│   │   │   │   ├── application_controller_renderer.rb
│   │   │   │   ├── assets.rb
│   │   │   │   ├── backtrace_silencers.rb
│   │   │   │   ├── content_security_policy.rb
│   │   │   │   ├── cookies_serializer.rb
│   │   │   │   ├── filter_parameter_logging.rb
│   │   │   │   ├── inflections.rb
│   │   │   │   ├── mime_types.rb
│   │   │   │   ├── permissions_policy.rb
│   │   │   │   ├── session_store.rb
│   │   │   │   └── wrap_parameters.rb
│   │   │   ├── locales/
│   │   │   │   └── en.yml
│   │   │   ├── puma.rb
│   │   │   ├── routes.rb
│   │   │   ├── secrets.yml
│   │   │   ├── spring.rb
│   │   │   └── storage.yml
│   │   ├── config.ru
│   │   ├── db/
│   │   │   ├── schema.rb
│   │   │   └── seeds.rb
│   │   ├── lib/
│   │   │   ├── assets/
│   │   │   │   └── .keep
│   │   │   └── tasks/
│   │   │       └── .keep
│   │   ├── log/
│   │   │   └── .keep
│   │   ├── public/
│   │   │   ├── 404.html
│   │   │   ├── 422.html
│   │   │   ├── 500.html
│   │   │   └── robots.txt
│   │   ├── tmp/
│   │   │   └── .keep
│   │   └── vendor/
│   │       └── assets/
│   │           ├── javascripts/
│   │           │   └── .keep
│   │           └── stylesheets/
│   │               └── .keep
│   ├── factories/
│   │   ├── app.rb
│   │   ├── job_definition.rb
│   │   ├── job_execution.rb
│   │   ├── job_queue.rb
│   │   ├── job_retry.rb
│   │   ├── retry_config.rb
│   │   ├── slack_notifications.rb
│   │   └── sns_subscription.rb
│   ├── models/
│   │   └── barbeque/
│   │       ├── job_definition_spec.rb
│   │       └── retry_config_spec.rb
│   ├── rails_helper.rb
│   ├── requests/
│   │   └── api/
│   │       ├── job_executions_spec.rb
│   │       ├── job_retries_spec.rb
│   │       └── revision_locks_spec.rb
│   ├── services/
│   │   └── message_enqueuing_service_spec.rb
│   └── spec_helper.rb
├── tools/
│   └── s3-log-migrator.rb
└── vendor/
    └── assets/
        ├── javascripts/
        │   ├── adminlte.js
        │   ├── bootstrap.js
        │   └── plotly-basic.js
        └── stylesheets/
            ├── AdminLTE.css
            ├── bootstrap.css
            └── skins/
                └── skin-blue.css
Download .txt
SYMBOL INDEX (1455 symbols across 91 files)

FILE: app/controllers/barbeque/api/application_controller.rb
  class Barbeque::Api::ApplicationController (line 3) | class Barbeque::Api::ApplicationController < ActionController::API
    method respond_with_error (line 21) | def respond_with_error(status_code, error_code, message)
    method force_json_format (line 26) | def force_json_format

FILE: app/controllers/barbeque/api/job_executions_controller.rb
  class Barbeque::Api::JobExecutionsController (line 3) | class Barbeque::Api::JobExecutionsController < Barbeque::Api::Applicatio...
    method require_resources (line 20) | def require_resources
    method require_resource (line 24) | def require_resource
    method create_resource (line 36) | def create_resource
    method enqueue_message (line 43) | def enqueue_message
    method location (line 54) | def location
    method respond_with_resource_options (line 62) | def respond_with_resource_options

FILE: app/controllers/barbeque/api/job_retries_controller.rb
  class Barbeque::Api::JobRetriesController (line 1) | class Barbeque::Api::JobRetriesController < Barbeque::Api::ApplicationCo...
    method require_resources (line 13) | def require_resources
    method create_resource (line 17) | def create_resource
    method retry_message (line 22) | def retry_message

FILE: app/controllers/barbeque/api/revision_locks_controller.rb
  class Barbeque::Api::RevisionLocksController (line 1) | class Barbeque::Api::RevisionLocksController < Barbeque::Api::Applicatio...
    method require_resources (line 10) | def require_resources
    method require_resource (line 14) | def require_resource
    method create_resource (line 18) | def create_resource
    method destroy_resource (line 27) | def destroy_resource

FILE: app/controllers/barbeque/application_controller.rb
  type Barbeque (line 1) | module Barbeque
    class ApplicationController (line 2) | class ApplicationController < ActionController::Base

FILE: app/controllers/barbeque/apps_controller.rb
  class Barbeque::AppsController (line 1) | class Barbeque::AppsController < Barbeque::ApplicationController
    method index (line 2) | def index
    method show (line 6) | def show
    method new (line 10) | def new
    method edit (line 14) | def edit
    method create (line 18) | def create
    method update (line 28) | def update
    method destroy (line 38) | def destroy

FILE: app/controllers/barbeque/job_definitions_controller.rb
  class Barbeque::JobDefinitionsController (line 1) | class Barbeque::JobDefinitionsController < Barbeque::ApplicationController
    method index (line 2) | def index
    method show (line 6) | def show
    method new (line 16) | def new
    method edit (line 25) | def edit
    method create (line 35) | def create
    method update (line 46) | def update
    method destroy (line 60) | def destroy
    method stats (line 69) | def stats
    method execution_stats (line 74) | def execution_stats
    method slack_notification_params (line 83) | def slack_notification_params
    method retry_config_params (line 87) | def retry_config_params
    method command_array (line 91) | def command_array
    method new_job_definition_params (line 95) | def new_job_definition_params

FILE: app/controllers/barbeque/job_executions_controller.rb
  class Barbeque::JobExecutionsController (line 1) | class Barbeque::JobExecutionsController < Barbeque::ApplicationController
    method show (line 4) | def show
    method retry (line 20) | def retry

FILE: app/controllers/barbeque/job_queues_controller.rb
  class Barbeque::JobQueuesController (line 5) | class Barbeque::JobQueuesController < Barbeque::ApplicationController
    method index (line 6) | def index
    method show (line 10) | def show
    method new (line 14) | def new
    method edit (line 18) | def edit
    method create (line 22) | def create
    method update (line 33) | def update
    method destroy (line 43) | def destroy
    method sqs_attributes (line 49) | def sqs_attributes
    method sqs_metrics (line 89) | def sqs_metrics
    method create_queue (line 134) | def create_queue(job_queue)
    method sqs_client (line 143) | def self.sqs_client
    method cloudwatch_client (line 147) | def self.cloudwatch_client
    method queue_name_from_arn (line 151) | def queue_name_from_arn(arn)
    method compute_minimum_period (line 155) | def compute_minimum_period(start_time, end_time, maximum_datapoint: 1440)

FILE: app/controllers/barbeque/job_retries_controller.rb
  class Barbeque::JobRetriesController (line 1) | class Barbeque::JobRetriesController < Barbeque::ApplicationController
    method show (line 2) | def show

FILE: app/controllers/barbeque/monitors_controller.rb
  class Barbeque::MonitorsController (line 1) | class Barbeque::MonitorsController < Barbeque::ApplicationController
    method index (line 2) | def index

FILE: app/controllers/barbeque/sns_subscriptions_controller.rb
  class Barbeque::SnsSubscriptionsController (line 1) | class Barbeque::SnsSubscriptionsController < Barbeque::ApplicationContro...
    method index (line 2) | def index
    method show (line 6) | def show
    method new (line 10) | def new
    method edit (line 14) | def edit
    method create (line 18) | def create
    method update (line 27) | def update
    method destroy (line 36) | def destroy

FILE: app/helpers/barbeque/application_helper.rb
  type Barbeque (line 1) | module Barbeque
    type ApplicationHelper (line 2) | module ApplicationHelper

FILE: app/helpers/barbeque/job_definitions_helper.rb
  type Barbeque::JobDefinitionsHelper (line 1) | module Barbeque::JobDefinitionsHelper
    function distance_of_time (line 2) | def distance_of_time(from, to)

FILE: app/helpers/barbeque/job_executions_helper.rb
  type Barbeque::JobExecutionsHelper (line 1) | module Barbeque::JobExecutionsHelper
    function status_label (line 2) | def status_label(status)

FILE: app/models/barbeque/api/application_resource.rb
  class Barbeque::Api::ApplicationResource (line 3) | class Barbeque::Api::ApplicationResource
    method initialize (line 9) | def initialize(model = nil)

FILE: app/models/barbeque/api/database_maintenance_resource.rb
  class Barbeque::Api::DatabaseMaintenanceResource (line 1) | class Barbeque::Api::DatabaseMaintenanceResource
    method initialize (line 8) | def initialize(exception)

FILE: app/models/barbeque/api/job_execution_resource.rb
  class Barbeque::Api::JobExecutionResource (line 1) | class Barbeque::Api::JobExecutionResource < Barbeque::Api::ApplicationRe...
    method html_url (line 10) | def html_url
    method message (line 18) | def message

FILE: app/models/barbeque/api/job_retry_resource.rb
  class Barbeque::Api::JobRetryResource (line 1) | class Barbeque::Api::JobRetryResource < Barbeque::Api::ApplicationResource

FILE: app/models/barbeque/api/revision_lock_resource.rb
  class Barbeque::Api::RevisionLockResource (line 1) | class Barbeque::Api::RevisionLockResource < Barbeque::Api::ApplicationRe...
    method revision (line 4) | def revision

FILE: app/models/barbeque/app.rb
  class Barbeque::App (line 1) | class Barbeque::App < Barbeque::ApplicationRecord

FILE: app/models/barbeque/application_record.rb
  type Barbeque (line 1) | module Barbeque
    class ApplicationRecord (line 2) | class ApplicationRecord < ActiveRecord::Base

FILE: app/models/barbeque/docker_container.rb
  class Barbeque::DockerContainer (line 1) | class Barbeque::DockerContainer < Barbeque::ApplicationRecord

FILE: app/models/barbeque/ecs_hako_task.rb
  class Barbeque::EcsHakoTask (line 1) | class Barbeque::EcsHakoTask < Barbeque::ApplicationRecord

FILE: app/models/barbeque/job_definition.rb
  class Barbeque::JobDefinition (line 1) | class Barbeque::JobDefinition < Barbeque::ApplicationRecord
    method execution_stats (line 20) | def execution_stats(from, to)

FILE: app/models/barbeque/job_execution.rb
  class Barbeque::JobExecution (line 1) | class Barbeque::JobExecution < Barbeque::ApplicationRecord
    method execution_log (line 20) | def execution_log
    method retryable? (line 24) | def retryable?
    method to_param (line 28) | def to_param
    method retry_if_possible! (line 32) | def retry_if_possible!

FILE: app/models/barbeque/job_queue.rb
  class Barbeque::JobQueue (line 3) | class Barbeque::JobQueue < Barbeque::ApplicationRecord
    method sqs_queue_name (line 16) | def sqs_queue_name
    method queue_url_from_name (line 30) | def self.queue_url_from_name(name)

FILE: app/models/barbeque/job_retry.rb
  class Barbeque::JobRetry (line 1) | class Barbeque::JobRetry < Barbeque::ApplicationRecord
    method execution_log (line 17) | def execution_log
    method to_resource (line 21) | def to_resource

FILE: app/models/barbeque/retry_config.rb
  class Barbeque::RetryConfig (line 1) | class Barbeque::RetryConfig < Barbeque::ApplicationRecord
    method should_retry? (line 8) | def should_retry?(retries)
    method delay_seconds (line 14) | def delay_seconds(retries)

FILE: app/models/barbeque/slack_notification.rb
  class Barbeque::SlackNotification (line 1) | class Barbeque::SlackNotification < Barbeque::ApplicationRecord

FILE: app/models/barbeque/sns_subscription.rb
  type Barbeque (line 1) | module Barbeque
    class SnsSubscription (line 2) | class SnsSubscription < ApplicationRecord
      method topic_region (line 12) | def topic_region
      method topic_arn_is_formatted (line 18) | def topic_arn_is_formatted

FILE: app/services/barbeque/message_enqueuing_service.rb
  class Barbeque::MessageEnqueuingService (line 3) | class Barbeque::MessageEnqueuingService
    class BadRequest (line 7) | class BadRequest < StandardError
    method sqs_client (line 10) | def self.sqs_client
    method initialize (line 19) | def initialize(application:, job:, message:, queue: nil, delay_seconds...
    method run (line 28) | def run
    method build_message (line 47) | def build_message

FILE: app/services/barbeque/message_retrying_service.rb
  class Barbeque::MessageRetryingService (line 3) | class Barbeque::MessageRetryingService
    method sqs_client (line 6) | def self.sqs_client
    method initialize (line 10) | def initialize(message_id:, delay_seconds: nil)
    method run (line 15) | def run
    method build_message (line 26) | def build_message

FILE: app/services/barbeque/sns_subscription_service.rb
  class Barbeque::SnsSubscriptionService (line 4) | class Barbeque::SnsSubscriptionService
    method sqs_client (line 5) | def self.sqs_client
    method sns_client (line 9) | def self.sns_client(region:)
    method subscribe (line 17) | def subscribe(sns_subscription)
    method unsubscribe (line 37) | def unsubscribe(sns_subscription)
    method sqs_client (line 46) | def sqs_client
    method sns_client (line 50) | def sns_client(region:)
    method update_sqs_policy! (line 55) | def update_sqs_policy!(sns_subscription)
    method generate_policy (line 78) | def generate_policy(queue_arn:, topic_arns:)
    method subscribe_topic! (line 96) | def subscribe_topic!(sns_subscription)
    method unsubscribe_topic! (line 111) | def unsubscribe_topic!(sns_subscription)

FILE: db/migrate/20160217020910_create_job_queues.rb
  class CreateJobQueues (line 1) | class CreateJobQueues < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160219010912_create_job_executions.rb
  class CreateJobExecutions (line 1) | class CreateJobExecutions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160223060807_create_apps.rb
  class CreateApps (line 1) | class CreateApps < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160223183348_create_job_definitions.rb
  class CreateJobDefinitions (line 1) | class CreateJobDefinitions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160225020801_add_job_definition_id_to_job_executions.rb
  class AddJobDefinitionIdToJobExecutions (line 1) | class AddJobDefinitionIdToJobExecutions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160412083604_add_finished_at_to_job_executions.rb
  class AddFinishedAtToJobExecutions (line 1) | class AddFinishedAtToJobExecutions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160415043427_create_slack_notifications.rb
  class CreateSlackNotifications (line 1) | class CreateSlackNotifications < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160509041452_add_job_queue_id_to_job_executions.rb
  class AddJobQueueIdToJobExecutions (line 1) | class AddJobQueueIdToJobExecutions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160516041710_create_job_retries.rb
  class CreateJobRetries (line 1) | class CreateJobRetries < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20160829023237_prefix_barbeque_to_tables.rb
  class PrefixBarbequeToTables (line 1) | class PrefixBarbequeToTables < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20170420030157_create_barbeque_sns_subscriptions.rb
  class CreateBarbequeSnsSubscriptions (line 1) | class CreateBarbequeSnsSubscriptions < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20170711085157_create_barbeque_docker_containers.rb
  class CreateBarbequeDockerContainers (line 1) | class CreateBarbequeDockerContainers < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20170712075449_create_barbeque_ecs_hako_tasks.rb
  class CreateBarbequeEcsHakoTasks (line 1) | class CreateBarbequeEcsHakoTasks < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20170724025542_add_index_to_job_execution_status.rb
  class AddIndexToJobExecutionStatus (line 1) | class AddIndexToJobExecutionStatus < ActiveRecord::Migration[5.0]
    method change (line 2) | def change

FILE: db/migrate/20180411070937_add_index_to_barbeque_job_executions_created_at.rb
  class AddIndexToBarbequeJobExecutionsCreatedAt (line 1) | class AddIndexToBarbequeJobExecutionsCreatedAt < ActiveRecord::Migration...
    method change (line 2) | def change

FILE: db/migrate/20190221050714_create_barbeque_retry_configs.rb
  class CreateBarbequeRetryConfigs (line 1) | class CreateBarbequeRetryConfigs < ActiveRecord::Migration[5.2]
    method change (line 2) | def change

FILE: db/migrate/20190311034445_add_notify_failure_only_if_retry_limit_reached_to_barbeque_slack_notifications.rb
  class AddNotifyFailureOnlyIfRetryLimitReachedToBarbequeSlackNotifications (line 1) | class AddNotifyFailureOnlyIfRetryLimitReachedToBarbequeSlackNotification...
    method change (line 2) | def change

FILE: db/migrate/20190315052951_change_barbeque_retry_configs_base_delay_default_value.rb
  class ChangeBarbequeRetryConfigsBaseDelayDefaultValue (line 1) | class ChangeBarbequeRetryConfigsBaseDelayDefaultValue < ActiveRecord::Mi...
    method up (line 2) | def up
    method down (line 6) | def down

FILE: db/migrate/20191029105530_change_job_name_to_case_sensitive.rb
  class ChangeJobNameToCaseSensitive (line 1) | class ChangeJobNameToCaseSensitive < ActiveRecord::Migration[5.2]
    method up (line 2) | def up
    method down (line 6) | def down

FILE: db/migrate/20240415080757_fix_collations.rb
  class FixCollations (line 8) | class FixCollations < ActiveRecord::Migration[6.1]
    method change (line 9) | def change

FILE: lib/barbeque/config.rb
  type Barbeque (line 4) | module Barbeque
    class Config (line 5) | class Config
      method initialize (line 8) | def initialize(options = {})
    type ConfigBuilder (line 20) | module ConfigBuilder
      function config (line 32) | def config
      function build_config (line 36) | def build_config(config_name = 'barbeque')

FILE: lib/barbeque/docker_image.rb
  type Barbeque (line 1) | module Barbeque
    class DockerImage (line 2) | class DockerImage
      method initialize (line 5) | def initialize(str)
      method to_s (line 16) | def to_s

FILE: lib/barbeque/engine.rb
  type Barbeque (line 4) | module Barbeque
    class Engine (line 5) | class Engine < ::Rails::Engine

FILE: lib/barbeque/exception_handler.rb
  type Barbeque (line 3) | module Barbeque
    type ExceptionHandler (line 4) | module ExceptionHandler
      function handler (line 10) | def handler
      class RailsLogger (line 15) | class RailsLogger
        method initialize (line 16) | def initialize
        method clear_context (line 20) | def clear_context
        method set_message_context (line 27) | def set_message_context(message_id, message_type)
        method handle_exception (line 33) | def handle_exception(e)
      class Raven (line 38) | class Raven
        method clear_context (line 39) | def clear_context
        method set_message_context (line 45) | def set_message_context(message_id, message_type)
        method handle_exception (line 50) | def handle_exception(e)
      class Sentry (line 55) | class Sentry
        method initialize (line 56) | def initialize
        method clear_context (line 60) | def clear_context
        method set_message_context (line 67) | def set_message_context(message_id, message_type)
        method handle_exception (line 77) | def handle_exception(e)

FILE: lib/barbeque/execution_log.rb
  type Barbeque (line 6) | module Barbeque
    class ExecutionLog (line 7) | class ExecutionLog
      method s3_client (line 13) | def s3_client
      method save_message (line 20) | def save_message(execution, message)
      method save_stdout_and_stderr (line 27) | def save_stdout_and_stderr(execution, stdout, stderr)
      method try_save_stdout_and_stderr (line 35) | def try_save_stdout_and_stderr(execution, stdout, stderr)
      method load (line 43) | def load(execution:)
      method s3_bucket_name (line 62) | def s3_bucket_name
      method s3_key_for (line 68) | def s3_key_for(execution, filename)
      method get (line 75) | def get(execution, filename)
      method put (line 88) | def put(execution, filename, content)

FILE: lib/barbeque/execution_poller.rb
  type Barbeque (line 4) | module Barbeque
    class ExecutionPoller (line 5) | class ExecutionPoller
      method initialize (line 6) | def initialize(job_queue)
      method run (line 11) | def run
      method stop (line 27) | def stop
      method poll (line 33) | def poll(job_execution)

FILE: lib/barbeque/executor.rb
  type Barbeque (line 5) | module Barbeque
    type Executor (line 23) | module Executor
      function create (line 24) | def self.create

FILE: lib/barbeque/executor/docker.rb
  type Barbeque (line 6) | module Barbeque
    type Executor (line 7) | module Executor
      class Docker (line 8) | class Docker
        class DockerCommandError (line 9) | class DockerCommandError < StandardError
        method initialize (line 12) | def initialize(**)
        method start_execution (line 17) | def start_execution(job_execution, envs)
        method start_retry (line 34) | def start_retry(job_retry, envs)
        method poll_execution (line 57) | def poll_execution(job_execution)
        method poll_retry (line 75) | def poll_retry(job_retry)
        method build_docker_run_command (line 102) | def build_docker_run_command(docker_image, command, envs)
        method env_options (line 106) | def env_options(envs)
        method inspect_container (line 114) | def inspect_container(container_id)
        method get_logs (line 130) | def get_logs(container_id)

FILE: lib/barbeque/executor/hako.rb
  type Barbeque (line 7) | module Barbeque
    type Executor (line 8) | module Executor
      class Hako (line 9) | class Hako
        class HakoCommandError (line 10) | class HakoCommandError < StandardError
        method initialize (line 19) | def initialize(hako_dir:, hako_env: {}, yaml_dir: nil, definition_...
        method start_execution (line 36) | def start_execution(job_execution, envs)
        method start_retry (line 55) | def start_retry(job_retry, envs)
        method poll_execution (line 80) | def poll_execution(job_execution)
        method poll_retry (line 99) | def poll_retry(job_retry)
        method build_hako_oneshot_command (line 123) | def build_hako_oneshot_command(docker_image, command, envs)
        method env_options (line 139) | def env_options(envs)
        method extract_task_info (line 145) | def extract_task_info(stdout)

FILE: lib/barbeque/hako_s3_client.rb
  type Barbeque (line 5) | module Barbeque
    class HakoS3Client (line 6) | class HakoS3Client
      method initialize (line 8) | def initialize(oneshot_notification_prefix)
      method s3_key_for_stopped_result (line 17) | def s3_key_for_stopped_result(hako_task)
      method s3_client (line 22) | def s3_client
      method get_stopped_result (line 28) | def get_stopped_result(hako_task)

FILE: lib/barbeque/maintenance.rb
  type Barbeque (line 1) | module Barbeque
    type Maintenance (line 2) | module Maintenance
      function database_maintenance_mode? (line 3) | def self.database_maintenance_mode?

FILE: lib/barbeque/message.rb
  type Barbeque (line 8) | module Barbeque
    type Message (line 9) | module Message
      function parse (line 13) | def parse(raw_message)
      function find_class (line 24) | def find_class(type)

FILE: lib/barbeque/message/base.rb
  type Barbeque (line 1) | module Barbeque
    type Message (line 2) | module Message
      class Base (line 4) | class Base
        method initialize (line 12) | def initialize(raw_message, message_body)
        method valid? (line 20) | def valid?
        method assign_body (line 26) | def assign_body(message_body)

FILE: lib/barbeque/message/invalid_message.rb
  type Barbeque (line 3) | module Barbeque
    type Message (line 4) | module Message
      class InvalidMessage (line 5) | class InvalidMessage < Base
        method valid? (line 6) | def valid?

FILE: lib/barbeque/message/job_execution.rb
  type Barbeque (line 3) | module Barbeque
    type Message (line 4) | module Message
      class JobExecution (line 5) | class JobExecution < Base
        method assign_body (line 12) | def assign_body(message_body)

FILE: lib/barbeque/message/job_retry.rb
  type Barbeque (line 3) | module Barbeque
    type Message (line 4) | module Message
      class JobRetry (line 5) | class JobRetry < Base
        method assign_body (line 10) | def assign_body(message_body)

FILE: lib/barbeque/message/notification.rb
  type Barbeque (line 3) | module Barbeque
    type Message (line 4) | module Message
      class Notification (line 5) | class Notification < Base
        method set_params_from_subscription (line 13) | def set_params_from_subscription(subscription)
        method assign_body (line 21) | def assign_body(message_body)

FILE: lib/barbeque/message_handler.rb
  type Barbeque (line 1) | module Barbeque
    type MessageHandler (line 2) | module MessageHandler
      class DuplicatedExecution (line 3) | class DuplicatedExecution < StandardError; end

FILE: lib/barbeque/message_handler/job_execution.rb
  type Barbeque (line 5) | module Barbeque
    type MessageHandler (line 6) | module MessageHandler
      class JobExecution (line 7) | class JobExecution
        method initialize (line 10) | def initialize(message:, message_queue:)
        method run (line 15) | def run
        method job_envs (line 30) | def job_envs
        method job_definition (line 41) | def job_definition
        method create_job_execution (line 48) | def create_job_execution

FILE: lib/barbeque/message_handler/job_retry.rb
  type Barbeque (line 5) | module Barbeque
    type MessageHandler (line 6) | module MessageHandler
      class MessageNotFound (line 7) | class MessageNotFound < StandardError; end
      class JobRetry (line 9) | class JobRetry
        method initialize (line 12) | def initialize(message:, message_queue:)
        method run (line 17) | def run
        method job_execution (line 33) | def job_execution
        method job_envs (line 37) | def job_envs
        method create_job_retry (line 52) | def create_job_retry

FILE: lib/barbeque/message_handler/notification.rb
  type Barbeque (line 3) | module Barbeque
    type MessageHandler (line 4) | module MessageHandler
      class Notification (line 5) | class Notification < JobExecution
        method initialize (line 8) | def initialize(message:, message_queue:)

FILE: lib/barbeque/message_queue.rb
  type Barbeque (line 5) | module Barbeque
    class MessageQueue (line 6) | class MessageQueue
      method initialize (line 9) | def initialize(job_queue)
      method dequeue (line 17) | def dequeue
      method delete_message (line 33) | def delete_message(message)
      method stop! (line 40) | def stop!
      method receive_message (line 46) | def receive_message
      method client (line 58) | def client

FILE: lib/barbeque/retry_poller.rb
  type Barbeque (line 4) | module Barbeque
    class RetryPoller (line 5) | class RetryPoller
      method initialize (line 6) | def initialize(job_queue)
      method run (line 11) | def run
      method stop (line 31) | def stop
      method poll (line 37) | def poll(job_retry)

FILE: lib/barbeque/runner.rb
  type Barbeque (line 7) | module Barbeque
    class Runner (line 12) | class Runner
      method initialize (line 13) | def initialize(job_queue)
      method run (line 17) | def run
      method stop (line 28) | def stop
      method message_queue (line 34) | def message_queue
      method keep_maximum_concurrent_executions (line 38) | def keep_maximum_concurrent_executions

FILE: lib/barbeque/slack_client.rb
  type Barbeque (line 5) | module Barbeque
    class SlackClient (line 6) | class SlackClient
      method initialize (line 7) | def initialize(channel)
      method notify_success (line 11) | def notify_success(message)
      method notify_failure (line 21) | def notify_failure(message)
      method post_slack (line 33) | def post_slack(payload)
      method default_payload (line 40) | def default_payload
      method endpoint_uri (line 44) | def endpoint_uri

FILE: lib/barbeque/slack_notifier.rb
  type Barbeque (line 3) | module Barbeque
    type SlackNotifier (line 4) | module SlackNotifier
      function notify_job_execution (line 7) | def notify_job_execution(job_execution)
      function notify_job_retry (line 31) | def notify_job_retry(job_retry)
      function barbeque_host (line 56) | def barbeque_host
      function job_execution_link (line 60) | def job_execution_link(job_execution)
      function job_execution_url (line 64) | def job_execution_url(job_execution)
      function job_retry_link (line 68) | def job_retry_link(job_retry)
      function job_retry_url (line 72) | def job_retry_url(job_retry)
      function should_notify_failure? (line 76) | def should_notify_failure?(job_execution_with_slack_notification)

FILE: lib/barbeque/version.rb
  type Barbeque (line 1) | module Barbeque

FILE: lib/barbeque/worker.rb
  type Barbeque (line 7) | module Barbeque
    type Worker (line 8) | module Worker
      class UnexpectedMessageType (line 11) | class UnexpectedMessageType < StandardError; end
      function run (line 13) | def self.run(
      function initialize (line 36) | def initialize
      function run (line 51) | def run
      function stop (line 63) | def stop
      function execute_command (line 68) | def execute_command

FILE: spec/barbeque/execution_log_spec.rb
  function make_s3_object (line 16) | def make_s3_object(content)

FILE: spec/barbeque/worker_spec.rb
  function worker_id (line 18) | def worker_id

FILE: spec/dummy/app/controllers/application_controller.rb
  class ApplicationController (line 1) | class ApplicationController < ActionController::Base

FILE: spec/dummy/app/helpers/application_helper.rb
  type ApplicationHelper (line 1) | module ApplicationHelper

FILE: spec/dummy/config/application.rb
  type Dummy (line 21) | module Dummy
    class Application (line 22) | class Application < Rails::Application

FILE: tools/s3-log-migrator.rb
  function safe_join (line 80) | def safe_join(thread)

FILE: vendor/assets/javascripts/adminlte.js
  function Plugin (line 98) | function Plugin(option) {
  function Plugin (line 277) | function Plugin(option) {
  function Plugin (line 436) | function Plugin(option) {
  function Plugin (line 505) | function Plugin(option) {
  function Plugin (line 680) | function Plugin(option) {
  function Plugin (line 786) | function Plugin(option) {
  function Plugin (line 944) | function Plugin(option) {
  function Plugin (line 1124) | function Plugin(option) {

FILE: vendor/assets/javascripts/bootstrap.js
  function transitionEnd (line 34) | function transitionEnd() {
  function removeElement (line 127) | function removeElement() {
  function Plugin (line 143) | function Plugin(option) {
  function Plugin (line 252) | function Plugin(option) {
  function Plugin (line 478) | function Plugin(option) {
  function getTargetFromTrigger (line 705) | function getTargetFromTrigger($trigger) {
  function Plugin (line 717) | function Plugin(option) {
  function getParent (line 784) | function getParent($this) {
  function clearMenus (line 797) | function clearMenus(e) {
  function Plugin (line 890) | function Plugin(option) {
  function Plugin (line 1234) | function Plugin(option, _relatedTarget) {
  function allowedAttribute (line 1361) | function allowedAttribute(attr, allowedAttributeList) {
  function sanitizeHtml (line 1386) | function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
  function complete (line 1757) | function complete() {
  function Plugin (line 1936) | function Plugin(option) {
  function Plugin (line 2060) | function Plugin(option) {
  function ScrollSpy (line 2103) | function ScrollSpy(element, options) {
  function Plugin (line 2223) | function Plugin(option) {
  function next (line 2332) | function next() {
  function Plugin (line 2378) | function Plugin(option) {
  function Plugin (line 2537) | function Plugin(option) {

FILE: vendor/assets/javascripts/plotly-basic.js
  function s (line 7) | function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&re...
  function d3_documentElement (line 266) | function d3_documentElement(node) {
  function d3_window (line 269) | function d3_window(node) {
  function d3_ascending (line 303) | function d3_ascending(a, b) {
  function d3_number (line 366) | function d3_number(x) {
  function d3_numeric (line 369) | function d3_numeric(x) {
  function d3_bisector (line 428) | function d3_bisector(compare) {
  function d3_transposeLength (line 489) | function d3_transposeLength(d) {
  function d3_range_integerScale (line 541) | function d3_range_integerScale(x) {
  function d3_class (line 546) | function d3_class(ctor, properties) {
  function d3_Map (line 568) | function d3_Map() {
  function d3_map_escape (line 601) | function d3_map_escape(key) {
  function d3_map_unescape (line 604) | function d3_map_unescape(key) {
  function d3_map_has (line 607) | function d3_map_has(key) {
  function d3_map_remove (line 610) | function d3_map_remove(key) {
  function d3_map_keys (line 613) | function d3_map_keys() {
  function d3_map_size (line 618) | function d3_map_size() {
  function d3_map_empty (line 623) | function d3_map_empty() {
  function map (line 629) | function map(mapType, array, depth) {
  function entries (line 653) | function entries(map, depth) {
  function d3_Set (line 695) | function d3_Set() {
  function d3_identity (line 713) | function d3_identity(d) {
  function d3_rebind (line 721) | function d3_rebind(target, source, method) {
  function d3_vendorSymbol (line 727) | function d3_vendorSymbol(object, name) {
  function d3_noop (line 736) | function d3_noop() {}
  function d3_dispatch (line 742) | function d3_dispatch() {}
  function d3_dispatch_event (line 757) | function d3_dispatch_event(dispatch) {
  function d3_eventPreventDefault (line 780) | function d3_eventPreventDefault() {
  function d3_eventSource (line 783) | function d3_eventSource() {
  function d3_eventDispatch (line 788) | function d3_eventDispatch(target) {
  function d3_selection (line 814) | function d3_selection(groups) {
  function d3_selection_selector (line 857) | function d3_selection_selector(selector) {
  function d3_selection_selectorAll (line 875) | function d3_selection_selectorAll(selector) {
  function d3_selection_attr (line 911) | function d3_selection_attr(name, value) {
  function d3_collapse (line 935) | function d3_collapse(s) {
  function d3_selection_classedRe (line 955) | function d3_selection_classedRe(name) {
  function d3_selection_classes (line 958) | function d3_selection_classes(name) {
  function d3_selection_classed (line 961) | function d3_selection_classed(name, value) {
  function d3_selection_classedName (line 974) | function d3_selection_classedName(name) {
  function d3_selection_style (line 1003) | function d3_selection_style(name, value, priority) {
  function d3_selection_property (line 1024) | function d3_selection_property(name, value) {
  function d3_selection_creator (line 1063) | function d3_selection_creator(name) {
  function d3_selectionRemove (line 1083) | function d3_selectionRemove() {
  function bind (line 1098) | function bind(group, groupData) {
  function d3_selection_dataNode (line 1168) | function d3_selection_dataNode(data) {
  function d3_selection_filter (line 1190) | function d3_selection_filter(selector) {
  function d3_selection_sortComparator (line 1211) | function d3_selection_sortComparator(comparator) {
  function d3_selection_each (line 1222) | function d3_selection_each(groups, callback) {
  function d3_selection_enter (line 1254) | function d3_selection_enter(selection) {
  function d3_selection_enterInsertBefore (line 1287) | function d3_selection_enterInsertBefore(enter) {
  function d3_selection_on (line 1332) | function d3_selection_on(type, listener, capture) {
  function d3_selection_onListener (line 1371) | function d3_selection_onListener(listener, argumentz) {
  function d3_selection_onFilter (line 1383) | function d3_selection_onFilter(listener, argumentz) {
  function d3_event_dragSuppress (line 1393) | function d3_event_dragSuppress(node) {
  function d3_mousePoint (line 1421) | function d3_mousePoint(container, e) {
  function drag (line 1460) | function drag() {
  function dragstart (line 1463) | function dragstart(id, position, subject, move, end) {
  function d3_behavior_dragTouchId (line 1507) | function d3_behavior_dragTouchId() {
  function d3_sgn (line 1519) | function d3_sgn(x) {
  function d3_cross2d (line 1522) | function d3_cross2d(a, b, c) {
  function d3_acos (line 1525) | function d3_acos(x) {
  function d3_asin (line 1528) | function d3_asin(x) {
  function d3_sinh (line 1531) | function d3_sinh(x) {
  function d3_cosh (line 1534) | function d3_cosh(x) {
  function d3_tanh (line 1537) | function d3_tanh(x) {
  function d3_haversin (line 1540) | function d3_haversin(x) {
  function zoom (line 1577) | function zoom(g) {
  function location (line 1678) | function location(p) {
  function point (line 1681) | function point(l) {
  function scaleTo (line 1684) | function scaleTo(s) {
  function translateTo (line 1687) | function translateTo(p, l) {
  function zoomTo (line 1692) | function zoomTo(that, p, l, k) {
  function rescale (line 1704) | function rescale() {
  function zoomstarted (line 1712) | function zoomstarted(dispatch) {
  function zoomed (line 1717) | function zoomed(dispatch) {
  function zoomended (line 1725) | function zoomended(dispatch) {
  function mousedowned (line 1730) | function mousedowned() {
  function touchstarted (line 1745) | function touchstarted() {
  function mousewheeled (line 1815) | function mousewheeled() {
  function dblclicked (line 1828) | function dblclicked() {
  function d3_color (line 1836) | function d3_color() {}
  function d3_hsl (line 1841) | function d3_hsl(h, s, l) {
  function d3_hsl_rgb (line 1856) | function d3_hsl_rgb(h, s, l) {
  function d3_hcl (line 1876) | function d3_hcl(h, c, l) {
  function d3_hcl_lab (line 1889) | function d3_hcl_lab(h, c, l) {
  function d3_lab (line 1895) | function d3_lab(l, a, b) {
  function d3_lab_rgb (line 1910) | function d3_lab_rgb(l, a, b) {
  function d3_lab_hcl (line 1917) | function d3_lab_hcl(l, a, b) {
  function d3_lab_xyz (line 1920) | function d3_lab_xyz(x) {
  function d3_xyz_lab (line 1923) | function d3_xyz_lab(x) {
  function d3_xyz_rgb (line 1926) | function d3_xyz_rgb(r) {
  function d3_rgb (line 1930) | function d3_rgb(r, g, b) {
  function d3_rgbNumber (line 1933) | function d3_rgbNumber(value) {
  function d3_rgbString (line 1936) | function d3_rgbString(value) {
  function d3_rgb_hex (line 1959) | function d3_rgb_hex(v) {
  function d3_rgb_parse (line 1962) | function d3_rgb_parse(format, rgb, hsl) {
  function d3_rgb_hsl (line 1998) | function d3_rgb_hsl(r, g, b) {
  function d3_rgb_lab (line 2010) | function d3_rgb_lab(r, g, b) {
  function d3_rgb_xyz (line 2017) | function d3_rgb_xyz(r) {
  function d3_rgb_parseNumber (line 2020) | function d3_rgb_parseNumber(c) {
  function d3_functor (line 2177) | function d3_functor(v) {
  function d3_xhrType (line 2184) | function d3_xhrType(response) {
  function d3_xhr (line 2191) | function d3_xhr(url, mimeType, response, callback) {
  function d3_xhr_fixCallback (line 2266) | function d3_xhr_fixCallback(callback) {
  function d3_xhrHasResponse (line 2271) | function d3_xhrHasResponse(request) {
  function dsv (line 2277) | function dsv(url, row, callback) {
  function response (line 2285) | function response(request) {
  function typedResponse (line 2288) | function typedResponse(f) {
  function token (line 2307) | function token() {
  function formatRow (line 2369) | function formatRow(row) {
  function formatValue (line 2372) | function formatValue(text) {
  function d3_timer (line 2385) | function d3_timer(callback, delay, then) {
  function d3_timer_step (line 2403) | function d3_timer_step() {
  function d3_timer_mark (line 2420) | function d3_timer_mark() {
  function d3_timer_sweep (line 2428) | function d3_timer_sweep() {
  function d3_format_precision (line 2441) | function d3_format_precision(x, p) {
  function d3_formatPrefix (line 2458) | function d3_formatPrefix(d, i) {
  function d3_locale_numberFormat (line 2469) | function d3_locale_numberFormat(locale) {
  function d3_format_typeDefault (line 2590) | function d3_format_typeDefault(x) {
  function d3_date_utc (line 2594) | function d3_date_utc() {
  function d3_time_interval (line 2660) | function d3_time_interval(local, step, number) {
  function d3_time_interval_utc (line 2708) | function d3_time_interval_utc(method) {
  function d3_locale_timeFormat (line 2768) | function d3_locale_timeFormat(locale) {
  function d3_time_formatPad (line 2989) | function d3_time_formatPad(value, fill, width) {
  function d3_time_formatRe (line 2993) | function d3_time_formatRe(names) {
  function d3_time_formatLookup (line 2996) | function d3_time_formatLookup(names) {
  function d3_time_parseWeekdayNumber (line 3001) | function d3_time_parseWeekdayNumber(date, string, i) {
  function d3_time_parseWeekNumberSunday (line 3006) | function d3_time_parseWeekNumberSunday(date, string, i) {
  function d3_time_parseWeekNumberMonday (line 3011) | function d3_time_parseWeekNumberMonday(date, string, i) {
  function d3_time_parseFullYear (line 3016) | function d3_time_parseFullYear(date, string, i) {
  function d3_time_parseYear (line 3021) | function d3_time_parseYear(date, string, i) {
  function d3_time_parseZone (line 3026) | function d3_time_parseZone(date, string, i) {
  function d3_time_expandYear (line 3030) | function d3_time_expandYear(d) {
  function d3_time_parseMonthNumber (line 3033) | function d3_time_parseMonthNumber(date, string, i) {
  function d3_time_parseDay (line 3038) | function d3_time_parseDay(date, string, i) {
  function d3_time_parseDayOfYear (line 3043) | function d3_time_parseDayOfYear(date, string, i) {
  function d3_time_parseHour24 (line 3048) | function d3_time_parseHour24(date, string, i) {
  function d3_time_parseMinutes (line 3053) | function d3_time_parseMinutes(date, string, i) {
  function d3_time_parseSeconds (line 3058) | function d3_time_parseSeconds(date, string, i) {
  function d3_time_parseMilliseconds (line 3063) | function d3_time_parseMilliseconds(date, string, i) {
  function d3_time_zone (line 3068) | function d3_time_zone(d) {
  function d3_time_parseLiteralPercent (line 3072) | function d3_time_parseLiteralPercent(date, string, i) {
  function d3_time_formatMulti (line 3077) | function d3_time_formatMulti(formats) {
  function d3_adder (line 3108) | function d3_adder() {}
  function d3_adderSum (line 3125) | function d3_adderSum(a, b, o) {
  function d3_geo_streamGeometry (line 3136) | function d3_geo_streamGeometry(geometry, listener) {
  function d3_geo_streamLine (line 3181) | function d3_geo_streamLine(coordinates, listener, closed) {
  function d3_geo_streamPolygon (line 3187) | function d3_geo_streamPolygon(coordinates, listener) {
  function d3_geo_areaRingStart (line 3216) | function d3_geo_areaRingStart() {
  function d3_geo_cartesian (line 3234) | function d3_geo_cartesian(spherical) {
  function d3_geo_cartesianDot (line 3238) | function d3_geo_cartesianDot(a, b) {
  function d3_geo_cartesianCross (line 3241) | function d3_geo_cartesianCross(a, b) {
  function d3_geo_cartesianAdd (line 3244) | function d3_geo_cartesianAdd(a, b) {
  function d3_geo_cartesianScale (line 3249) | function d3_geo_cartesianScale(vector, k) {
  function d3_geo_cartesianNormalize (line 3252) | function d3_geo_cartesianNormalize(d) {
  function d3_geo_spherical (line 3258) | function d3_geo_spherical(cartesian) {
  function d3_geo_sphericalEqual (line 3261) | function d3_geo_sphericalEqual(a, b) {
  function point (line 3286) | function point(λ, φ) {
  function linePoint (line 3291) | function linePoint(λ, φ) {
  function lineStart (line 3331) | function lineStart() {
  function lineEnd (line 3334) | function lineEnd() {
  function ringPoint (line 3339) | function ringPoint(λ, φ) {
  function ringStart (line 3347) | function ringStart() {
  function ringEnd (line 3350) | function ringEnd() {
  function angle (line 3357) | function angle(λ0, λ1) {
  function compareRanges (line 3360) | function compareRanges(a, b) {
  function withinRange (line 3363) | function withinRange(x, range) {
  function d3_geo_centroidPoint (line 3417) | function d3_geo_centroidPoint(λ, φ) {
  function d3_geo_centroidPointXYZ (line 3422) | function d3_geo_centroidPointXYZ(x, y, z) {
  function d3_geo_centroidLineStart (line 3428) | function d3_geo_centroidLineStart() {
  function d3_geo_centroidLineEnd (line 3449) | function d3_geo_centroidLineEnd() {
  function d3_geo_centroidRingStart (line 3452) | function d3_geo_centroidRingStart() {
  function d3_geo_compose (line 3482) | function d3_geo_compose(a, b) {
  function d3_true (line 3491) | function d3_true() {
  function d3_geo_clipPolygon (line 3494) | function d3_geo_clipPolygon(segments, compare, clipStartInside, interpol...
  function d3_geo_clipPolygonLinkCircular (line 3553) | function d3_geo_clipPolygonLinkCircular(array) {
  function d3_geo_clipPolygonIntersection (line 3564) | function d3_geo_clipPolygonIntersection(point, points, other, entry) {
  function d3_geo_clip (line 3572) | function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) {
  function d3_geo_clipSegmentLength1 (line 3664) | function d3_geo_clipSegmentLength1(segment) {
  function d3_geo_clipBufferListener (line 3667) | function d3_geo_clipBufferListener() {
  function d3_geo_clipSort (line 3688) | function d3_geo_clipSort(a, b) {
  function d3_geo_clipAntimeridianLine (line 3692) | function d3_geo_clipAntimeridianLine(listener) {
  function d3_geo_clipAntimeridianIntersect (line 3731) | function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) {
  function d3_geo_clipAntimeridianInterpolate (line 3735) | function d3_geo_clipAntimeridianInterpolate(from, to, direction, listene...
  function d3_geo_pointInPolygon (line 3758) | function d3_geo_pointInPolygon(point, polygon) {
  function d3_geo_clipCircle (line 3787) | function d3_geo_clipCircle(radius) {
  function d3_geom_clipLine (line 3883) | function d3_geom_clipLine(x0, y0, x1, y1) {
  function d3_geo_clipExtent (line 3955) | function d3_geo_clipExtent(x0, y0, x1, y1) {
  function d3_geo_conic (line 4089) | function d3_geo_conic(projectAt) {
  function d3_geo_conicEqualArea (line 4097) | function d3_geo_conicEqualArea(φ0, φ1) {
  function albersUsa (line 4124) | function albersUsa(coordinates) {
  function d3_geo_pathAreaRingStart (line 4206) | function d3_geo_pathAreaRingStart() {
  function d3_geo_pathBoundsPoint (line 4228) | function d3_geo_pathBoundsPoint(x, y) {
  function d3_geo_pathBuffer (line 4234) | function d3_geo_pathBuffer() {
  function d3_geo_pathBufferCircle (line 4279) | function d3_geo_pathBufferCircle(radius) {
  function d3_geo_pathCentroidPoint (line 4295) | function d3_geo_pathCentroidPoint(x, y) {
  function d3_geo_pathCentroidLineStart (line 4300) | function d3_geo_pathCentroidLineStart() {
  function d3_geo_pathCentroidLineEnd (line 4314) | function d3_geo_pathCentroidLineEnd() {
  function d3_geo_pathCentroidRingStart (line 4317) | function d3_geo_pathCentroidRingStart() {
  function d3_geo_pathContext (line 4338) | function d3_geo_pathContext(context) {
  function d3_geo_resample (line 4378) | function d3_geo_resample(project) {
  function path (line 4458) | function path(object) {
  function reset (line 4497) | function reset() {
  function d3_geo_pathProjectStream (line 4503) | function d3_geo_pathProjectStream(project) {
  function d3_geo_transform (line 4520) | function d3_geo_transform(stream) {
  function d3_geo_transformPoint (line 4543) | function d3_geo_transformPoint(stream, point) {
  function d3_geo_projection (line 4565) | function d3_geo_projection(project) {
  function d3_geo_projectionMutator (line 4570) | function d3_geo_projectionMutator(projectAt) {
  function d3_geo_projectionRadians (line 4642) | function d3_geo_projectionRadians(stream) {
  function d3_geo_equirectangular (line 4647) | function d3_geo_equirectangular(λ, φ) {
  function forward (line 4655) | function forward(coordinates) {
  function d3_geo_identityRotation (line 4665) | function d3_geo_identityRotation(λ, φ) {
  function d3_geo_rotation (line 4669) | function d3_geo_rotation(δλ, δφ, δγ) {
  function d3_geo_forwardRotationλ (line 4672) | function d3_geo_forwardRotationλ(δλ) {
  function d3_geo_rotationλ (line 4677) | function d3_geo_rotationλ(δλ) {
  function d3_geo_rotationφγ (line 4682) | function d3_geo_rotationφγ(δφ, δγ) {
  function circle (line 4696) | function circle() {
  function d3_geo_circleInterpolate (line 4726) | function d3_geo_circleInterpolate(radius, precision) {
  function d3_geo_circleAngle (line 4743) | function d3_geo_circleAngle(cr, point) {
  function graticule (line 4756) | function graticule() {
  function lines (line 4762) | function lines() {
  function d3_geo_graticuleX (line 4828) | function d3_geo_graticuleX(y0, y1, dy) {
  function d3_geo_graticuleY (line 4836) | function d3_geo_graticuleY(x0, x1, dx) {
  function d3_source (line 4844) | function d3_source(d) {
  function d3_target (line 4847) | function d3_target(d) {
  function greatArc (line 4852) | function greatArc() {
  function d3_geo_interpolate (line 4879) | function d3_geo_interpolate(x0, y0, x1, y1) {
  function d3_geo_lengthLineStart (line 4904) | function d3_geo_lengthLineStart() {
  function d3_geo_azimuthal (line 4919) | function d3_geo_azimuthal(scale, angle) {
  function d3_geo_conicConformal (line 4945) | function d3_geo_conicConformal(φ0, φ1) {
  function d3_geo_conicEquidistant (line 4968) | function d3_geo_conicEquidistant(φ0, φ1) {
  function d3_geo_mercator (line 4990) | function d3_geo_mercator(λ, φ) {
  function d3_geo_mercatorProjection (line 4996) | function d3_geo_mercatorProjection(project) {
  function d3_geo_transverseMercator (line 5037) | function d3_geo_transverseMercator(λ, φ) {
  function d3_geom_pointX (line 5055) | function d3_geom_pointX(d) {
  function d3_geom_pointY (line 5058) | function d3_geom_pointY(d) {
  function hull (line 5064) | function hull(data) {
  function d3_geom_hullUpper (line 5086) | function d3_geom_hullUpper(points) {
  function d3_geom_hullOrder (line 5094) | function d3_geom_hullOrder(a, b) {
  function d3_geom_polygonInside (line 5148) | function d3_geom_polygonInside(p, a, b) {
  function d3_geom_polygonIntersect (line 5151) | function d3_geom_polygonIntersect(c, d, a, b) {
  function d3_geom_polygonClosed (line 5155) | function d3_geom_polygonClosed(coordinates) {
  function d3_geom_voronoiBeach (line 5160) | function d3_geom_voronoiBeach() {
  function d3_geom_voronoiCreateBeach (line 5164) | function d3_geom_voronoiCreateBeach(site) {
  function d3_geom_voronoiDetachBeach (line 5169) | function d3_geom_voronoiDetachBeach(beach) {
  function d3_geom_voronoiRemoveBeach (line 5175) | function d3_geom_voronoiRemoveBeach(beach) {
  function d3_geom_voronoiAddBeach (line 5211) | function d3_geom_voronoiAddBeach(site) {
  function d3_geom_voronoiLeftBreakPoint (line 5265) | function d3_geom_voronoiLeftBreakPoint(arc, directrix) {
  function d3_geom_voronoiRightBreakPoint (line 5277) | function d3_geom_voronoiRightBreakPoint(arc, directrix) {
  function d3_geom_voronoiCell (line 5283) | function d3_geom_voronoiCell(site) {
  function d3_geom_voronoiCloseCells (line 5296) | function d3_geom_voronoiCloseCells(extent) {
  function d3_geom_voronoiHalfEdgeOrder (line 5326) | function d3_geom_voronoiHalfEdgeOrder(a, b) {
  function d3_geom_voronoiCircle (line 5329) | function d3_geom_voronoiCircle() {
  function d3_geom_voronoiAttachCircle (line 5333) | function d3_geom_voronoiAttachCircle(arc) {
  function d3_geom_voronoiDetachCircle (line 5366) | function d3_geom_voronoiDetachCircle(arc) {
  function d3_geom_voronoiClipEdges (line 5376) | function d3_geom_voronoiClipEdges(extent) {
  function d3_geom_voronoiConnectEdge (line 5386) | function d3_geom_voronoiConnectEdge(edge, extent) {
  function d3_geom_voronoiEdge (line 5460) | function d3_geom_voronoiEdge(lSite, rSite) {
  function d3_geom_voronoiCreateEdge (line 5465) | function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) {
  function d3_geom_voronoiCreateBorderEdge (line 5474) | function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) {
  function d3_geom_voronoiSetEdgeEnd (line 5481) | function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) {
  function d3_geom_voronoiHalfEdge (line 5492) | function d3_geom_voronoiHalfEdge(edge, lSite, rSite) {
  function d3_geom_voronoiRedBlackTree (line 5506) | function d3_geom_voronoiRedBlackTree() {
  function d3_geom_voronoiRedBlackNode (line 5509) | function d3_geom_voronoiRedBlackNode(node) {
  function d3_geom_voronoiRedBlackRotateLeft (line 5672) | function d3_geom_voronoiRedBlackRotateLeft(tree, node) {
  function d3_geom_voronoiRedBlackRotateRight (line 5685) | function d3_geom_voronoiRedBlackRotateRight(tree, node) {
  function d3_geom_voronoiRedBlackFirst (line 5698) | function d3_geom_voronoiRedBlackFirst(node) {
  function d3_geom_voronoi (line 5702) | function d3_geom_voronoi(sites, bbox) {
  function d3_geom_voronoiVertexOrder (line 5731) | function d3_geom_voronoiVertexOrder(a, b) {
  function voronoi (line 5737) | function voronoi(data) {
  function sites (line 5748) | function sites(data) {
  function d3_geom_voronoiTriangleArea (line 5801) | function d3_geom_voronoiTriangleArea(a, b, c) {
  function quadtree (line 5819) | function quadtree(data) {
  function d3_geom_quadtreeCompatX (line 5914) | function d3_geom_quadtreeCompatX(d) {
  function d3_geom_quadtreeCompatY (line 5917) | function d3_geom_quadtreeCompatY(d) {
  function d3_geom_quadtreeNode (line 5920) | function d3_geom_quadtreeNode() {
  function d3_geom_quadtreeVisit (line 5929) | function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) {
  function d3_geom_quadtreeFind (line 5938) | function d3_geom_quadtreeFind(root, x, y, x0, y0, x3, y3) {
  function d3_interpolateRgb (line 5975) | function d3_interpolateRgb(a, b) {
  function d3_interpolateObject (line 5984) | function d3_interpolateObject(a, b) {
  function d3_interpolateNumber (line 6004) | function d3_interpolateNumber(a, b) {
  function d3_interpolateString (line 6011) | function d3_interpolateString(a, b) {
  function d3_interpolate (line 6045) | function d3_interpolate(a, b) {
  function d3_interpolateArray (line 6055) | function d3_interpolateArray(a, b) {
  function d3_ease_clamp (line 6106) | function d3_ease_clamp(f) {
  function d3_ease_reverse (line 6111) | function d3_ease_reverse(f) {
  function d3_ease_reflect (line 6116) | function d3_ease_reflect(f) {
  function d3_ease_quad (line 6121) | function d3_ease_quad(t) {
  function d3_ease_cubic (line 6124) | function d3_ease_cubic(t) {
  function d3_ease_cubicInOut (line 6127) | function d3_ease_cubicInOut(t) {
  function d3_ease_poly (line 6133) | function d3_ease_poly(e) {
  function d3_ease_sin (line 6138) | function d3_ease_sin(t) {
  function d3_ease_exp (line 6141) | function d3_ease_exp(t) {
  function d3_ease_circle (line 6144) | function d3_ease_circle(t) {
  function d3_ease_elastic (line 6147) | function d3_ease_elastic(a, p) {
  function d3_ease_back (line 6155) | function d3_ease_back(s) {
  function d3_ease_bounce (line 6161) | function d3_ease_bounce(t) {
  function d3_interpolateHcl (line 6165) | function d3_interpolateHcl(a, b) {
  function d3_interpolateHsl (line 6176) | function d3_interpolateHsl(a, b) {
  function d3_interpolateLab (line 6187) | function d3_interpolateLab(a, b) {
  function d3_interpolateRound (line 6196) | function d3_interpolateRound(a, b) {
  function d3_transform (line 6212) | function d3_transform(m) {
  function d3_transformDot (line 6228) | function d3_transformDot(a, b) {
  function d3_transformNormalize (line 6231) | function d3_transformNormalize(a) {
  function d3_transformCombine (line 6239) | function d3_transformCombine(a, b, k) {
  function d3_interpolateTransformPop (line 6253) | function d3_interpolateTransformPop(s) {
  function d3_interpolateTranslate (line 6256) | function d3_interpolateTranslate(ta, tb, s, q) {
  function d3_interpolateRotate (line 6270) | function d3_interpolateRotate(ra, rb, s, q) {
  function d3_interpolateSkew (line 6281) | function d3_interpolateSkew(wa, wb, s, q) {
  function d3_interpolateScale (line 6291) | function d3_interpolateScale(ka, kb, s, q) {
  function d3_interpolateTransform (line 6305) | function d3_interpolateTransform(a, b) {
  function d3_uninterpolateNumber (line 6319) | function d3_uninterpolateNumber(a, b) {
  function d3_uninterpolateClamp (line 6325) | function d3_uninterpolateClamp(a, b) {
  function d3_layout_bundlePath (line 6339) | function d3_layout_bundlePath(link) {
  function d3_layout_bundleAncestors (line 6352) | function d3_layout_bundleAncestors(node) {
  function d3_layout_bundleLeastCommonAncestor (line 6362) | function d3_layout_bundleLeastCommonAncestor(a, b) {
  function relayout (line 6374) | function relayout() {
  function resort (line 6440) | function resort() {
  function repulse (line 6487) | function repulse(node) {
  function position (line 6668) | function position(dimension, size) {
  function dragmove (line 6697) | function dragmove(d) {
  function d3_layout_forceDragstart (line 6703) | function d3_layout_forceDragstart(d) {
  function d3_layout_forceDragend (line 6706) | function d3_layout_forceDragend(d) {
  function d3_layout_forceMouseover (line 6709) | function d3_layout_forceMouseover(d) {
  function d3_layout_forceMouseout (line 6713) | function d3_layout_forceMouseout(d) {
  function d3_layout_forceAccumulate (line 6716) | function d3_layout_forceAccumulate(quad, alpha, charges) {
  function hierarchy (line 6746) | function hierarchy(root) {
  function d3_layout_hierarchyRebind (line 6802) | function d3_layout_hierarchyRebind(object, hierarchy) {
  function d3_layout_hierarchyVisitBefore (line 6808) | function d3_layout_hierarchyVisitBefore(node, callback) {
  function d3_layout_hierarchyVisitAfter (line 6818) | function d3_layout_hierarchyVisitAfter(node, callback) {
  function d3_layout_hierarchyChildren (line 6831) | function d3_layout_hierarchyChildren(d) {
  function d3_layout_hierarchyValue (line 6834) | function d3_layout_hierarchyValue(d) {
  function d3_layout_hierarchySort (line 6837) | function d3_layout_hierarchySort(a, b) {
  function d3_layout_hierarchyLinks (line 6840) | function d3_layout_hierarchyLinks(nodes) {
  function position (line 6852) | function position(node, x, dx, dy) {
  function depth (line 6867) | function depth(node) {
  function partition (line 6875) | function partition(d, i) {
  function pie (line 6889) | function pie(data) {
  function stack (line 6939) | function stack(data, index) {
  function d3_layout_stackX (line 6994) | function d3_layout_stackX(d) {
  function d3_layout_stackY (line 6997) | function d3_layout_stackY(d) {
  function d3_layout_stackOut (line 7000) | function d3_layout_stackOut(d, y0, y) {
  function d3_layout_stackOrderDefault (line 7067) | function d3_layout_stackOrderDefault(data) {
  function d3_layout_stackOffsetZero (line 7070) | function d3_layout_stackOffsetZero(data) {
  function d3_layout_stackMaxIndex (line 7075) | function d3_layout_stackMaxIndex(array) {
  function d3_layout_stackReduceSum (line 7085) | function d3_layout_stackReduceSum(d) {
  function d3_layout_stackSum (line 7088) | function d3_layout_stackSum(p, d) {
  function histogram (line 7093) | function histogram(data, i) {
  function d3_layout_histogramBinSturges (line 7137) | function d3_layout_histogramBinSturges(range, values) {
  function d3_layout_histogramBinFixed (line 7140) | function d3_layout_histogramBinFixed(range, n) {
  function d3_layout_histogramRange (line 7145) | function d3_layout_histogramRange(values) {
  function pack (line 7150) | function pack(d, i) {
  function d3_layout_packSort (line 7189) | function d3_layout_packSort(a, b) {
  function d3_layout_packInsert (line 7192) | function d3_layout_packInsert(a, b) {
  function d3_layout_packSplice (line 7199) | function d3_layout_packSplice(a, b) {
  function d3_layout_packIntersects (line 7203) | function d3_layout_packIntersects(a, b) {
  function d3_layout_packSiblings (line 7207) | function d3_layout_packSiblings(node) {
  function d3_layout_packLink (line 7271) | function d3_layout_packLink(node) {
  function d3_layout_packUnlink (line 7274) | function d3_layout_packUnlink(node) {
  function d3_layout_packTransform (line 7278) | function d3_layout_packTransform(node, x, y, k) {
  function d3_layout_packPlace (line 7288) | function d3_layout_packPlace(a, b, c) {
  function tree (line 7304) | function tree(d, i) {
  function wrapTree (line 7323) | function wrapTree(root0) {
  function firstWalk (line 7347) | function firstWalk(v) {
  function secondWalk (line 7363) | function secondWalk(v) {
  function apportion (line 7367) | function apportion(v, w, ancestor) {
  function sizeNode (line 7397) | function sizeNode(node) {
  function d3_layout_treeSeparation (line 7418) | function d3_layout_treeSeparation(a, b) {
  function d3_layout_treeLeft (line 7421) | function d3_layout_treeLeft(v) {
  function d3_layout_treeRight (line 7425) | function d3_layout_treeRight(v) {
  function d3_layout_treeMove (line 7429) | function d3_layout_treeMove(wm, wp, shift) {
  function d3_layout_treeShift (line 7437) | function d3_layout_treeShift(v) {
  function d3_layout_treeAncestor (line 7446) | function d3_layout_treeAncestor(vim, v, ancestor) {
  function cluster (line 7451) | function cluster(d, i) {
  function d3_layout_clusterY (line 7491) | function d3_layout_clusterY(children) {
  function d3_layout_clusterX (line 7496) | function d3_layout_clusterX(children) {
  function d3_layout_clusterLeft (line 7501) | function d3_layout_clusterLeft(node) {
  function d3_layout_clusterRight (line 7505) | function d3_layout_clusterRight(node) {
  function scale (line 7511) | function scale(children, k) {
  function squarify (line 7518) | function squarify(node) {
  function stickify (line 7545) | function stickify(node) {
  function worst (line 7562) | function worst(row, u) {
  function position (line 7573) | function position(row, u, rect, flush) {
  function treemap (line 7603) | function treemap(d) {
  function padFunction (line 7620) | function padFunction(node) {
  function padConstant (line 7624) | function padConstant(node) {
  function d3_layout_treemapPadNull (line 7655) | function d3_layout_treemapPadNull(node) {
  function d3_layout_treemapPad (line 7663) | function d3_layout_treemapPad(node, padding) {
  function d3_scaleExtent (line 7715) | function d3_scaleExtent(domain) {
  function d3_scaleRange (line 7719) | function d3_scaleRange(scale) {
  function d3_scale_bilinear (line 7722) | function d3_scale_bilinear(domain, range, uninterpolate, interpolate) {
  function d3_scale_nice (line 7728) | function d3_scale_nice(domain, nice) {
  function d3_scale_niceStep (line 7738) | function d3_scale_niceStep(step) {
  function d3_scale_polylinear (line 7752) | function d3_scale_polylinear(domain, range, uninterpolate, interpolate) {
  function d3_scale_linear (line 7770) | function d3_scale_linear(domain, range, interpolate, clamp) {
  function d3_scale_linearRebind (line 7822) | function d3_scale_linearRebind(scale, linear) {
  function d3_scale_linearNice (line 7825) | function d3_scale_linearNice(domain, m) {
  function d3_scale_linearTickRange (line 7830) | function d3_scale_linearTickRange(domain, m) {
  function d3_scale_linearTicks (line 7839) | function d3_scale_linearTicks(domain, m) {
  function d3_scale_linearTickFormat (line 7842) | function d3_scale_linearTickFormat(domain, m, format) {
  function d3_scale_linearPrecision (line 7870) | function d3_scale_linearPrecision(value) {
  function d3_scale_linearFormatPrecision (line 7873) | function d3_scale_linearFormatPrecision(type, range) {
  function d3_scale_log (line 7880) | function d3_scale_log(linear, base, positive, domain) {
  function d3_scale_pow (line 7953) | function d3_scale_pow(linear, exponent, domain) {
  function d3_scale_powPow (line 7987) | function d3_scale_powPow(e) {
  function d3_scale_ordinal (line 8001) | function d3_scale_ordinal(domain, ranger) {
  function d3_scale_quantile (line 8109) | function d3_scale_quantile(domain, range) {
  function d3_scale_quantize (line 8145) | function d3_scale_quantize(x0, x1, range) {
  function d3_scale_threshold (line 8179) | function d3_scale_threshold(domain, range) {
  function d3_scale_identity (line 8205) | function d3_scale_identity(domain) {
  function d3_zero (line 8227) | function d3_zero() {
  function arc (line 8232) | function arc() {
  function circleSegment (line 8310) | function circleSegment(r1, cw) {
  function d3_svg_arcInnerRadius (line 8355) | function d3_svg_arcInnerRadius(d) {
  function d3_svg_arcOuterRadius (line 8358) | function d3_svg_arcOuterRadius(d) {
  function d3_svg_arcStartAngle (line 8361) | function d3_svg_arcStartAngle(d) {
  function d3_svg_arcEndAngle (line 8364) | function d3_svg_arcEndAngle(d) {
  function d3_svg_arcPadAngle (line 8367) | function d3_svg_arcPadAngle(d) {
  function d3_svg_arcSweep (line 8370) | function d3_svg_arcSweep(x0, y0, x1, y1) {
  function d3_svg_arcCornerTangents (line 8373) | function d3_svg_arcCornerTangents(p0, p1, r1, rc, cw) {
  function d3_svg_line (line 8378) | function d3_svg_line(projection) {
  function d3_svg_lineLinear (line 8445) | function d3_svg_lineLinear(points) {
  function d3_svg_lineLinearClosed (line 8448) | function d3_svg_lineLinearClosed(points) {
  function d3_svg_lineStep (line 8451) | function d3_svg_lineStep(points) {
  function d3_svg_lineStepBefore (line 8457) | function d3_svg_lineStepBefore(points) {
  function d3_svg_lineStepAfter (line 8462) | function d3_svg_lineStepAfter(points) {
  function d3_svg_lineCardinalOpen (line 8467) | function d3_svg_lineCardinalOpen(points, tension) {
  function d3_svg_lineCardinalClosed (line 8470) | function d3_svg_lineCardinalClosed(points, tension) {
  function d3_svg_lineCardinal (line 8474) | function d3_svg_lineCardinal(points, tension) {
  function d3_svg_lineHermite (line 8477) | function d3_svg_lineHermite(points, tangents) {
  function d3_svg_lineCardinalTangents (line 8504) | function d3_svg_lineCardinalTangents(points, tension) {
  function d3_svg_lineBasis (line 8514) | function d3_svg_lineBasis(points) {
  function d3_svg_lineBasisOpen (line 8530) | function d3_svg_lineBasisOpen(points) {
  function d3_svg_lineBasisClosed (line 8550) | function d3_svg_lineBasisClosed(points) {
  function d3_svg_lineBundle (line 8569) | function d3_svg_lineBundle(points, tension) {
  function d3_svg_lineDot4 (line 8582) | function d3_svg_lineDot4(a, b) {
  function d3_svg_lineBasisBezier (line 8586) | function d3_svg_lineBasisBezier(path, x, y) {
  function d3_svg_lineSlope (line 8589) | function d3_svg_lineSlope(p0, p1) {
  function d3_svg_lineFiniteDifferences (line 8592) | function d3_svg_lineFiniteDifferences(points) {
  function d3_svg_lineMonotoneTangents (line 8600) | function d3_svg_lineMonotoneTangents(points) {
  function d3_svg_lineMonotone (line 8624) | function d3_svg_lineMonotone(points) {
  function d3_svg_lineRadial (line 8633) | function d3_svg_lineRadial(points) {
  function d3_svg_area (line 8644) | function d3_svg_area(projection) {
  function chord (line 8734) | function chord(d, i) {
  function subgroup (line 8738) | function subgroup(self, f, d, i) {
  function equals (line 8748) | function equals(a, b) {
  function arc (line 8751) | function arc(r, p, a) {
  function curve (line 8754) | function curve(r0, p0, r1, p1) {
  function d3_svg_chordRadius (line 8784) | function d3_svg_chordRadius(d) {
  function diagonal (line 8789) | function diagonal(d, i) {
  function d3_svg_diagonalProjection (line 8817) | function d3_svg_diagonalProjection(d) {
  function d3_svg_diagonalRadialProjection (line 8827) | function d3_svg_diagonalRadialProjection(projection) {
  function symbol (line 8835) | function symbol(d, i) {
  function d3_svg_symbolSize (line 8850) | function d3_svg_symbolSize() {
  function d3_svg_symbolType (line 8853) | function d3_svg_symbolType() {
  function d3_svg_symbolCircle (line 8856) | function d3_svg_symbolCircle(size) {
  function d3_selection_interruptNS (line 8905) | function d3_selection_interruptNS(ns) {
  function d3_transition (line 8917) | function d3_transition(groups, ns, id) {
  function d3_transition_tween (line 8989) | function d3_transition_tween(groups, name, value, tween) {
  function attrNull (line 9003) | function attrNull() {
  function attrNullNS (line 9006) | function attrNullNS() {
  function attrTween (line 9009) | function attrTween(b) {
  function attrTweenNS (line 9017) | function attrTweenNS(b) {
  function attrTween (line 9029) | function attrTween(d, i) {
  function attrTweenNS (line 9035) | function attrTweenNS(d, i) {
  function styleNull (line 9053) | function styleNull() {
  function styleString (line 9056) | function styleString(b) {
  function styleTween (line 9068) | function styleTween(d, i) {
  function d3_transition_text (line 9079) | function d3_transition_text(b) {
  function d3_transitionNamespace (line 9159) | function d3_transitionNamespace(name) {
  function d3_transitionNode (line 9162) | function d3_transitionNode(node, i, ns, id, inherit) {
  function axis (line 9239) | function axis(g) {
  function d3_svg_axisX (line 9334) | function d3_svg_axisX(selection, x0, x1) {
  function d3_svg_axisY (line 9340) | function d3_svg_axisY(selection, y0, y1) {
  function brush (line 9348) | function brush(g) {
  function redraw (line 9434) | function redraw(g) {
  function redrawX (line 9439) | function redrawX(g) {
  function redrawY (line 9443) | function redrawY(g) {
  function brushstart (line 9447) | function brushstart() {
  function d3_time_formatIsoNative (line 9640) | function d3_time_formatIsoNative(date) {
  function d3_time_scale (line 9687) | function d3_time_scale(linear, methods, format) {
  function d3_time_scaleDate (line 9737) | function d3_time_scaleDate(t) {
  function d3_json (line 9796) | function d3_json(request) {
  function d3_html (line 9802) | function d3_html(request) {
  function objectOrFunction (line 9828) | function objectOrFunction(x) {
  function isFunction (line 9832) | function isFunction(x) {
  function setScheduler (line 9867) | function setScheduler(scheduleFn) {
  function setAsap (line 9871) | function setAsap(asapFn) {
  function useNextTick (line 9884) | function useNextTick() {
  function useVertxTimer (line 9893) | function useVertxTimer() {
  function useMutationObserver (line 9899) | function useMutationObserver() {
  function useMessageChannel (line 9911) | function useMessageChannel() {
  function useSetTimeout (line 9919) | function useSetTimeout() {
  function flush (line 9929) | function flush() {
  function attemptVertx (line 9943) | function attemptVertx() {
  function then (line 9968) | function then(onFulfillment, onRejection) {
  function resolve (line 10026) | function resolve(object) {
  function noop (line 10041) | function noop() {}
  function selfFulfillment (line 10049) | function selfFulfillment() {
  function cannotReturnOwn (line 10053) | function cannotReturnOwn() {
  function getThen (line 10057) | function getThen(promise) {
  function tryThen (line 10066) | function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
  function handleForeignThenable (line 10074) | function handleForeignThenable(promise, thenable, then) {
  function handleOwnThenable (line 10103) | function handleOwnThenable(promise, thenable) {
  function handleMaybeThenable (line 10117) | function handleMaybeThenable(promise, maybeThenable, then$$) {
  function _resolve (line 10133) | function _resolve(promise, value) {
  function publishRejection (line 10143) | function publishRejection(promise) {
  function fulfill (line 10151) | function fulfill(promise, value) {
  function _reject (line 10164) | function _reject(promise, reason) {
  function subscribe (line 10174) | function subscribe(parent, child, onFulfillment, onRejection) {
  function publish (line 10189) | function publish(promise) {
  function ErrorObject (line 10215) | function ErrorObject() {
  function tryCatch (line 10221) | function tryCatch(callback, detail) {
  function invokeCallback (line 10230) | function invokeCallback(settled, promise, callback, detail) {
  function initializePromise (line 10270) | function initializePromise(promise, resolver) {
  function nextId (line 10283) | function nextId() {
  function makePromise (line 10287) | function makePromise(promise) {
  function Enumerator (line 10294) | function Enumerator(Constructor, input) {
  function validationError (line 10323) | function validationError() {
  function all (line 10437) | function all(entries) {
  function race (line 10506) | function race(entries) {
  function reject (line 10558) | function reject(reason) {
  function needsResolver (line 10566) | function needsResolver() {
  function needsNew (line 10570) | function needsNew() {
  function Promise (line 10677) | function Promise(resolver) {
  function polyfill (line 10926) | function polyfill() {
  function EventEmitter (line 10991) | function EventEmitter() {
  function g (line 11129) | function g() {
  function isFunction (line 11257) | function isFunction(arg) {
  function isNumber (line 11261) | function isNumber(arg) {
  function isObject (line 11265) | function isObject(arg) {
  function isUndefined (line 11269) | function isUndefined(arg) {
  function allBlankCharCodes (line 11302) | function allBlankCharCodes(str){
  function fromQuat (line 11340) | function fromQuat(out, q) {
  function mouseEventOffset (line 11401) | function mouseEventOffset (ev, target, out) {
  function getBoundingClientOffset (line 11414) | function getBoundingClientOffset (element) {
  function defaultSetTimout (line 11436) | function defaultSetTimout() {
  function defaultClearTimeout (line 11439) | function defaultClearTimeout () {
  function runTimeout (line 11462) | function runTimeout(fun) {
  function runClearTimeout (line 11487) | function runClearTimeout(marker) {
  function cleanUpNextTick (line 11519) | function cleanUpNextTick() {
  function drainQueue (line 11534) | function drainQueue() {
  function Item (line 11572) | function Item(fun, array) {
  function noop (line 11586) | function noop() {}
  function tinycolor (line 11625) | function tinycolor (color, opts) {
  function inputToRGB (line 11915) | function inputToRGB(color) {
  function rgbToRgb (line 11979) | function rgbToRgb(r, g, b){
  function rgbToHsl (line 11991) | function rgbToHsl(r, g, b) {
  function hslToRgb (line 12022) | function hslToRgb(h, s, l) {
  function rgbToHsv (line 12056) | function rgbToHsv(r, g, b) {
  function hsvToRgb (line 12086) | function hsvToRgb(h, s, v) {
  function rgbToHex (line 12109) | function rgbToHex(r, g, b, allow3Char) {
  function rgbaToHex (line 12129) | function rgbaToHex(r, g, b, a, allow4Char) {
  function rgbaToArgbHex (line 12149) | function rgbaToArgbHex(r, g, b, a) {
  function desaturate (line 12182) | function desaturate(color, amount) {
  function saturate (line 12190) | function saturate(color, amount) {
  function greyscale (line 12198) | function greyscale(color) {
  function lighten (line 12202) | function lighten (color, amount) {
  function brighten (line 12210) | function brighten(color, amount) {
  function darken (line 12219) | function darken (color, amount) {
  function spin (line 12229) | function spin(color, amount) {
  function complement (line 12241) | function complement(color) {
  function triad (line 12247) | function triad(color) {
  function tetrad (line 12257) | function tetrad(color) {
  function splitcomplement (line 12268) | function splitcomplement(color) {
  function analogous (line 12278) | function analogous(color, results, slices) {
  function monochromatic (line 12293) | function monochromatic(color, results) {
  function flip (line 12575) | function flip(o) {
  function boundAlpha (line 12586) | function boundAlpha(a) {
  function bound01 (line 12597) | function bound01(n, max) {
  function clamp01 (line 12618) | function clamp01(val) {
  function parseIntFromHex (line 12623) | function parseIntFromHex(val) {
  function isOnePointZero (line 12629) | function isOnePointZero(n) {
  function isPercentage (line 12634) | function isPercentage(n) {
  function pad2 (line 12639) | function pad2(c) {
  function convertToPercentage (line 12644) | function convertToPercentage(n) {
  function convertDecimalToHex (line 12653) | function convertDecimalToHex(d) {
  function convertHexToDecimal (line 12657) | function convertHexToDecimal(h) {
  function isValidCSSUnit (line 12696) | function isValidCSSUnit(color) {
  function stringInputToObject (line 12703) | function stringInputToObject(color) {
  function validateWCAG2Parms (line 12776) | function validateWCAG2Parms(parms) {
  function coerce (line 12829) | function coerce(attr, dflt) {
  function annAutorange (line 13290) | function annAutorange(gd) {
  function hasClickToShow (line 13382) | function hasClickToShow(gd, hoverData) {
  function onClick (line 13397) | function onClick(gd, hoverData) {
  function getToggleSets (line 13431) | function getToggleSets(gd, hoverData) {
  function clickData2r (line 13482) | function clickData2r(d, ax) {
  function convert (line 13595) | function convert(attr) {
  function draw (line 13688) | function draw(gd) {
  function drawOne (line 13707) | function drawOne(gd, index) {
  function drawRaw (line 13727) | function drawRaw(gd, options, index, subplotId, xa, ya) {
  function hideLine (line 14461) | function hideLine() { el3.style('stroke-dasharray', '0px,100px'); }
  function drawhead (line 14463) | function drawhead(p, rot) {
  function mockAnnAxes (line 14630) | function mockAnnAxes(ann, scene) {
  function handleAnnotationDefaults (line 14693) | function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, item...
  function cleanOne (line 15002) | function cleanOne(val) {
  function coerce (line 15217) | function coerce(attr, dflt) {
  function component (line 15319) | function component() {
  function getTrace (line 15857) | function getTrace() {
  function parseScale (line 16260) | function parseScale() {
  function colorArray2rbga (line 16502) | function colorArray2rbga(colorArray) {
  function onStart (line 16793) | function onStart(e) {
  function onMove (line 16836) | function onMove(e) {
  function onDone (line 16854) | function onDone(e) {
  function coverSlip (line 16912) | function coverSlip() {
  function finishDrag (line 16932) | function finishDrag(gd) {
  function pointerOffset (line 16937) | function pointerOffset(e) {
  function singlePointStyle (line 17280) | function singlePointStyle(d, sel, trace, markerScale, lineScale, marker,...
  function makeTangent (line 17557) | function makeTangent(prevpt, thispt, nextpt, smoothness) {
  function nodeHash (line 17782) | function nodeHash(node) {
  function calcOneAxis (line 18557) | function calcOneAxis(calcTrace, trace, axis, coord) {
  function makeComputeErrorValue (line 18661) | function makeComputeErrorValue(type, value) {
  function coerce (line 18703) | function coerce(attr, dflt) {
  function errorCoords (line 18952) | function errorCoords(d, xa, ya) {
  function makeCoerceHoverInfo (line 19082) | function makeCoerceHoverInfo(trace) {
  function paste (line 19112) | function paste(traceAttr, cd, cdAttr, fn) {
  function emitClick (line 19145) | function emitClick() { gd.emit('plotly_click', {points: gd._hoverdata, e...
  function coerce (line 19206) | function coerce(attr, dflt) {
  function quadrature (line 19293) | function quadrature(dx, dy) {
  function _hover (line 19507) | function _hover(gd, evt, subplot, noHoverEvent) {
  function createHoverText (line 19863) | function createHoverText(hoverData, opts, gd) {
  function hoverAvoidOverlaps (line 20168) | function hoverAvoidOverlaps(hoverData, ax) {
  function alignHoverText (line 20323) | function alignHoverText(hoverLabels, rotateLabels) {
  function cleanPoint (line 20377) | function cleanPoint(d, hovermode) {
  function createSpikelines (line 20487) | function createSpikelines(hoverData, opts) {
  function hoverChanged (line 20620) | function hoverChanged(gd, evt, oldhoverdata) {
  function loneUnhover (line 20709) | function loneUnhover(containerOrSelection) {
  function castHoverOption (line 20722) | function castHoverOption(trace, ptNumber, attr) {
  function castHoverinfo (line 20726) | function castHoverinfo(trace, fullLayout, ptNumber) {
  function coerce (line 20809) | function coerce(attr, dflt) {
  function isHoriz (line 20835) | function isHoriz(fullData) {
  function coerce (line 20866) | function coerce(attr, dflt) {
  function imageDefaults (line 21106) | function imageDefaults(imageIn, imageOut, fullLayout) {
  function setImage (line 21213) | function setImage(d) {
  function applyAttributes (line 21262) | function applyAttributes(d) {
  function coerce (line 21593) | function coerce(attr, dflt) {
  function scrollHandler (line 21932) | function scrollHandler(scrollBarY, scrollBoxY) {
  function drawTexts (line 21998) | function drawTexts(g, gd) {
  function setupTraceToggle (line 22052) | function setupTraceToggle(g, gd) {
  function handleClick (line 22098) | function handleClick(g, gd, numClicks) {
  function computeTextDimensions (line 22189) | function computeTextDimensions(g, gd) {
  function computeLegendDimensions (line 22233) | function computeLegendDimensions(gd, groups, traces) {
  function expandMargin (line 22373) | function expandMargin(gd) {
  function expandHorizontalMargin (line 22404) | function expandHorizontalMargin(gd) {
  function addOneItem (line 22452) | function addOneItem(legendGroup, legendItem) {
  function styleLines (line 22649) | function styleLines(d) {
  function stylePoints (line 22674) | function stylePoints(d) {
  function styleBars (line 22758) | function styleBars(d) {
  function styleBoxes (line 22783) | function styleBoxes(d) {
  function stylePies (line 22806) | function stylePies(d) {
  function handleCartesian (line 22995) | function handleCartesian(gd, ev) {
  function handleDrag3d (line 23113) | function handleDrag3d(gd, ev) {
  function handleCamera3d (line 23146) | function handleCamera3d(gd, ev) {
  function handleHover3d (line 23180) | function handleHover3d(gd, ev) {
  function handleGeo (line 23268) | function handleGeo(gd, ev) {
  function toggleHover (line 23310) | function toggleHover(gd) {
  function setSpikelineVisibility (line 23382) | function setSpikelineVisibility(gd) {
  function getButtonGroups (line 23511) | function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
  function areAllAxesFixed (line 23596) | function areAllAxesFixed(fullLayout) {
  function isSelectable (line 23612) | function isSelectable(fullData) {
  function appendButtonsToGroups (line 23636) | function appendButtonsToGroups(groups, buttons) {
  function fillCustomButton (line 23650) | function fillCustomButton(customButtons) {
  function ModeBar (line 23700) | function ModeBar(opts) {
  function createModeBar (line 23948) | function createModeBar(gd, buttons) {
  function coerce (line 24151) | function coerce(attr, dflt) {
  function buttonsDefaults (line 24176) | function buttonsDefaults(containerIn, containerOut, calendar) {
  function getPosDflt (line 24213) | function getPosDflt(containerOut, layout, counterAxes) {
  function makeSelectorData (line 24320) | function makeSelectorData(gd) {
  function selectorKeyFunc (line 24335) | function selectorKeyFunc(d) {
  function isActive (line 24339) | function isActive(axisLayout, opts, update) {
  function drawButtonRect (line 24353) | function drawButtonRect(button, selectorLayout, d) {
  function getFillColor (line 24372) | function getFillColor(selectorLayout, d) {
  function drawButtonText (line 24378) | function drawButtonText(button, selectorLayout, d, gd) {
  function getLabel (line 24397) | function getLabel(opts) {
  function reposition (line 24405) | function reposition(gd, buttons, opts, axName) {
  function getXRange (line 24525) | function getXRange(axisLayout, buttonLayout) {
  function coerce (line 24756) | function coerce(attr, dflt) {
  function keyFunction (line 24837) | function keyFunction(axisOpts) {
  function makeRangeSliderData (line 24942) | function makeRangeSliderData(fullLayout) {
  function setupDragElement (line 24958) | function setupDragElement(rangeSlider, gd, axisOpts, opts) {
  function setDataRange (line 25027) | function setDataRange(rangeSlider, gd, axisOpts, opts) {
  function setPixelRange (line 25041) | function setPixelRange(rangeSlider, gd, axisOpts, opts) {
  function drawBg (line 25080) | function drawBg(rangeSlider, gd, axisOpts, opts) {
  function addClipPath (line 25109) | function addClipPath(rangeSlider, gd, axisOpts, opts) {
  function drawRangePlot (line 25126) | function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
  function filterRangePlotCalcData (line 25194) | function filterRangePlotCalcData(calcData, subplotId) {
  function drawMasks (line 25209) | function drawMasks(rangeSlider, gd, axisOpts, opts) {
  function drawSlideBox (line 25235) | function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
  function drawGrabbers (line 25253) | function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
  function clearPushMargins (line 25325) | function clearPushMargins(gd) {
  function shapeBounds (line 25507) | function shapeBounds(ax, v0, v1, path, paramsToUse) {
  function draw (line 25667) | function draw(gd) {
  function drawOne (line 25685) | function drawOne(gd, index) {
  function setupDragElement (line 25749) | function setupDragElement(gd, shapePath, shapeOptions, index) {
  function getPathString (line 25903) | function getPathString(gd, options) {
  function convertPath (line 25955) | function convertPath(pathIn, x2p, y2p) {
  function movePath (line 25982) | function movePath(pathIn, moveX, moveY) {
  function coerce (line 26135) | function coerce(attr, dflt) {
  function sliderDefaults (line 26575) | function sliderDefaults(sliderIn, sliderOut, layoutOut) {
  function stepsDefaults (line 26629) | function stepsDefaults(sliderIn, sliderOut) {
  function makeSliderData (line 26754) | function makeSliderData(fullLayout, gd) {
  function keyFunction (line 26769) | function keyFunction(opts) {
  function findDimensions (line 26774) | function findDimensions(gd, sliderOpts) {
  function drawSlider (line 26894) | function drawSlider(gd, sliderGroup, sliderOpts) {
  function drawCurrentValue (line 26921) | function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) {
  function drawGrip (line 26980) | function drawGrip(sliderGroup, gd, sliderOpts) {
  function drawLabel (line 27000) | function drawLabel(item, data, sliderOpts) {
  function drawLabelGroup (line 27019) | function drawLabelGroup(sliderGroup, sliderOpts) {
  function handleInput (line 27053) | function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, do...
  function setActive (line 27061) | function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTra...
  function attachGripEvents (line 27100) | function attachGripEvents(item, gd, sliderGroup) {
  function drawTicks (line 27148) | function drawTicks(sliderGroup, sliderOpts) {
  function computeLabelSteps (line 27178) | function computeLabelSteps(sliderOpts) {
  function setGripPosition (line 27191) | function setGripPosition(sliderGroup, sliderOpts, position, doTransition) {
  function normalizedValueToPosition (line 27213) | function normalizedValueToPosition(sliderOpts, normalizedPosition) {
  function positionToNormalizedValue (line 27219) | function positionToNormalizedValue(sliderOpts, position) {
  function drawTouchRect (line 27223) | function drawTouchRect(sliderGroup, gd, sliderOpts) {
  function drawRail (line 27242) | function drawRail(sliderGroup, sliderOpts) {
  function clearPushMargins (line 27268) | function clearPushMargins(gd) {
  function titleLayout (line 27416) | function titleLayout(titleEl) {
  function drawTitle (line 27420) | function drawTitle(titleEl) {
  function scootTitle (line 27439) | function scootTitle(titleElIn) {
  function setPlaceholder (line 27508) | function setPlaceholder() {
  function menuDefaults (line 27808) | function menuDefaults(menuIn, menuOut, layoutOut) {
  function buttonsDefaults (line 27843) | function buttonsDefaults(menuIn, menuOut) {
  function makeMenuData (line 28012) | function makeMenuData(fullLayout) {
  function keyFunction (line 28033) | function keyFunction(menuOpts) {
  function isFolded (line 28037) | function isFolded(gButton) {
  function isActive (line 28041) | function isActive(gButton, menuOpts) {
  function setActive (line 28045) | function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox...
  function drawHeader (line 28064) | function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
  function drawButtons (line 28128) | function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
  function drawScrollBox (line 28250) | function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, positi...
  function hideScrollBox (line 28288) | function hideScrollBox(scrollBox) {
  function drawItem (line 28313) | function drawItem(item, menuOpts, itemOpts, gd) {
  function drawItemRect (line 28318) | function drawItemRect(item, menuOpts) {
  function drawItemText (line 28335) | function drawItemText(item, menuOpts, itemOpts, gd) {
  function styleButtons (line 28352) | function styleButtons(buttons, menuOpts) {
  function styleOnMouseOver (line 28365) | function styleOnMouseOver(item) {
  function styleOnMouseOut (line 28370) | function styleOnMouseOut(item, menuOpts) {
  function findDimensions (line 28376) | function findDimensions(gd, menuOpts) {
  function setItemPosition (line 28503) | function setItemPosition(item, menuOpts, posOpts, overrideOpts) {
  function removeAllButtons (line 28538) | function removeAllButtons(gButton) {
  function clearPushMargins (line 28542) | function clearPushMargins(gd) {
  function ScrollBox (line 28585) | function ScrollBox(gd, container, id) {
  function isWorldCalendar (line 29758) | function isWorldCalendar(calendar) {
  function lpad (line 29947) | function lpad(val, digits) {
  function includeTime (line 30037) | function includeTime(dateStr, h, m, s, msec10) {
  function modDateFormat (line 30091) | function modDateFormat(fmt, x, calendar) {
  function formatTime (line 30121) | function formatTime(x, tr) {
  function yearFormatWorld (line 30162) | function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); }
  function monthFormatWorld (line 30163) | function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); }
  function dayFormatWorld (line 30164) | function dayFormatWorld(cDate) { return cDate.formatDate('M d'); }
  function yearMonthDayFormatWorld (line 30165) | function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, ...
  function primitivesLoopSplice (line 30559) | function primitivesLoopSplice(source, target) {
  function _extend (line 30606) | function _extend(inputs, isDeep, keepAllKeys, noArrayCopies) {
  function segmentsIntersect (line 30758) | function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
  function perpDistance2 (line 30808) | function perpDistance2(xab, yab, l2_ab, xac, yac) {
  function getDistToPlot (line 30900) | function getDistToPlot(len) {
  function continueAsync (line 31260) | function continueAsync() {
  function apply (line 31800) | function apply(f, args) {
  function npGet (line 32020) | function npGet(cont, parts) {
  function isDeletable (line 32081) | function isDeletable(val, propStr) {
  function npSet (line 32099) | function npSet(cont, parts, propStr) {
  function joinPropStr (line 32145) | function joinPropStr(propStr, newPart) {
  function setArrayAll (line 32154) | function setArrayAll(containerArray, innerParts, val, propStr) {
  function checkNewContainer (line 32183) | function checkNewContainer(container, part, nextPart, toDelete) {
  function pruneContainers (line 32193) | function pruneContainers(containerLevels) {
  function emptyObj (line 32228) | function emptyObj(obj) {
  function badContainer (line 32235) | function badContainer(container, propStr, propParts) {
  function killNote (line 32303) | function killNote(transition) {
  function rectContains (line 32465) | function rectContains(pt, omitFirstEdge) {
  function contains (line 32478) | function contains(pt, omitFirstEdge) {
  function addPt (line 32603) | function addPt(pt) {
  function copyArgArray (line 32694) | function copyArgArray(gd, args) {
  function lessThan (line 32993) | function lessThan(a, b) { return a < b; }
  function lessOrEqual (line 32994) | function lessOrEqual(a, b) { return a <= b; }
  function greaterThan (line 32995) | function greaterThan(a, b) { return a > b; }
  function greaterOrEqual (line 32996) | function greaterOrEqual(a, b) { return a >= b; }
  function getSize (line 33192) | function getSize(_selection, _dimension) {
  function showText (line 33223) | function showText() {
  function cleanEscapesForTex (line 33330) | function cleanEscapesForTex(s) {
  function texToSVG (line 33335) | function texToSVG(_texString, _config, _callback) {
  function getQuotedMatch (line 33434) | function getQuotedMatch(_str, re) {
  function replaceFromMapObject (line 33448) | function replaceFromMapObject(_str, list) {
  function convertEntities (line 33459) | function convertEntities(_str) {
  function buildSVGText (line 33473) | function buildSVGText(containerNode, str) {
  function setOrGet (line 33656) | function setOrGet(attr, val) {
  function alignHTMLWith (line 33677) | function alignHTMLWith(_base, container, options) {
  function handleClick (line 33738) | function handleClick() {
  function selectElementContents (line 33751) | function selectElementContents(_el) {
  function appendEditable (line 33761) | function appendEditable() {
  function cleanAxRef (line 34185) | function cleanAxRef(container, attr) {
  function cleanTextPosition (line 34379) | function cleanTextPosition(textposition) {
  function emptyContainer (line 34391) | function emptyContainer(outer, innerStr) {
  function getParent (line 34493) | function getParent(attr) {
  function addFrames (line 34816) | function addFrames() {
  function drawFramework (line 34919) | function drawFramework() {
  function marginPushers (line 34934) | function marginPushers() {
  function marginPushersAgain (line 34957) | function marginPushersAgain() {
  function positionAndAutorange (line 34966) | function positionAndAutorange() {
  function doAutoRangeAndConstraints (line 35003) | function doAutoRangeAndConstraints() {
  function drawAxes (line 35022) | function drawAxes() {
  function drawData (line 35027) | function drawData() {
  function finalDraw (line 35089) | function finalDraw() {
  function opaqueSetBackground (line 35126) | function opaqueSetBackground(gd, bgColor) {
  function setPlotContext (line 35131) | function setPlotContext(gd, config) {
  function plotPolar (line 35197) | function plotPolar(gd, data, layout) {
  function positivifyIndices (line 35333) | function positivifyIndices(indices, maxIndex) {
  function assertIndexArray (line 35359) | function assertIndexArray(gd, indices, arrayName) {
  function checkMoveTracesArgs (line 35392) | function checkMoveTracesArgs(gd, currentIndices, newIndices) {
  function checkAddTracesArgs (line 35428) | function checkAddTracesArgs(gd, traces, newIndices) {
  function assertExtendTracesArgs (line 35475) | function assertExtendTracesArgs(gd, update, indices, maxPoints) {
  function getExtendProperties (line 35523) | function getExtendProperties(gd, update, indices, maxPoints) {
  function spliceTraces (line 35599) | function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spl...
  function _restyle (line 36027) | function _restyle(gd, aobj, _traces) {
  function _relayout (line 36613) | function _relayout(gd, aobj) {
  function getTransitionOpts (line 37188) | function getTransitionOpts(i) {
  function getFrameOpts (line 37200) | function getFrameOpts(i) {
  function callbackOnNthTime (line 37217) | function callbackOnNthTime(cb, n) {
  function discardExistingFrames (line 37227) | function discardExistingFrames() {
  function queueFrames (line 37242) | function queueFrames(frameList) {
  function stopAnimationLoop (line 37301) | function stopAnimationLoop() {
  function nextFrame (line 37309) | function nextFrame() {
  function beginAnimationLoop (line 37358) | function beginAnimationLoop() {
  function setTransitionConfig (line 37385) | function setTransitionConfig(frame) {
  function makePlotFramework (line 37720) | function makePlotFramework(gd) {
  function defaultSetBackground (line 37960) | function defaultSetBackground(gd, bgColor) {
  function callback (line 38119) | function callback(attr, attrName, attrs, level) {
  function toAttrString (line 38144) | function toAttrString(stack) {
  function getTraceAttributes (line 38182) | function getTraceAttributes(type) {
  function getLayoutAttributes (line 38240) | function getLayoutAttributes() {
  function getTransformAttributes (line 38287) | function getTransformAttributes(type) {
  function getFramesAttributes (line 38307) | function getFramesAttributes() {
  function formatAttributes (line 38317) | function formatAttributes(attrs) {
  function mergeValTypeAndRole (line 38324) | function mergeValTypeAndRole(attrs) {
  function formatArrayContainers (line 38356) | function formatArrayContainers(attrs) {
  function assignPolarLayoutAttrs (line 38375) | function assignPolarLayoutAttrs(layoutAttributes) {
  function handleBasePlotModule (line 38386) | function handleBasePlotModule(layoutAttributes, _module, astr) {
  function insertAttrs (line 38394) | function insertAttrs(baseAttrs, newAttrs, astr) {
  function registerTraceModule (line 38449) | function registerTraceModule(newModule) {
  function registerTransformModule (line 38457) | function registerTransformModule(newModule) {
  function registerComponentModule (line 38491) | function registerComponentModule(newModule) {
  function overlappingDomain (line 38554) | function overlappingDomain(xDomain, yDomain, domains) {
  function xLinePath (line 38780) | function xLinePath(y, showThis) {
  function yLinePath (line 38785) | function yLinePath(x, showThis) {
  function shouldShowLine (line 38878) | function shouldShowLine(ax, counterAx, side) {
  function findCounterAxes (line 38884) | function findCounterAxes(gd, ax, axList) {
  function findLineWidth (line 38903) | function findLineWidth(gd, axes, side) {
  function findCounterAxisLineWidth (line 38913) | function findCounterAxisLineWidth(gd, ax, subplotCounterLineWidth,
  function toImage (line 39084) | function toImage(gd, opts) {
  function crawl (line 39315) | function crawl(objIn, objOut, schema, list, base, path) {
  function fillLayoutSchema (line 39399) | function fillLayoutSchema(schema, dataOut) {
  function inBase (line 39462) | function inBase(base) {
  function format (line 39468) | function format(code, base, path, valIn, valOut) {
  function isInSchema (line 39501) | function isInSchema(schema, key) {
  function getNestedSchema (line 39513) | function getNestedSchema(schema, key) {
  function splitKey (line 39519) | function splitKey(key) {
  function convertPathToAttributeString (line 39531) | function convertPathToAttributeString(path) {
  function getPad (line 40360) | function getPad(item) {
  function addItem (line 40376) | function addItem(i) {
  function autoShiftNumericBins (line 40548) | function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) {
  function autoShiftMonthBins (line 40597) | function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) {
  function arrayTicks (line 40730) | function arrayTicks(ax) {
  function roundDTick (line 40780) | function roundDTick(roughDTick, base, roundingSet) {
  function autoTickRound (line 40892) | function autoTickRound(ax) {
  function isHidden (line 41077) | function isHidden(showAttr) {
  function tickTextObj (line 41105) | function tickTextObj(ax, x, text) {
  function formatDate (line 41119) | function formatDate(ax, out, hover, extraPrecision) {
  function formatLog (line 41175) | function formatLog(ax, out, hover, extraPrecision, hideexp) {
  function formatCategory (line 41222) | function formatCategory(ax, out) {
  function formatLinear (line 41228) | function formatLinear(ax, out, hover, extraPrecision, hideexp) {
  function numFormat (line 41244) | function numFormat(v, ax, fmtoverride, hover) {
  function hasAx2 (line 41385) | function hasAx2(sp, ax2) {
  function clipEnds (line 41646) | function clipEnds(d) {
  function drawTicks (line 41652) | function drawTicks(container, tickpath) {
  function drawLabels (line 41667) | function drawLabels(container, position) {
  function drawAxTitle (line 41898) | function drawAxTitle() {
  function traceHasBarsOrFill (line 41978) | function traceHasBarsOrFill(trace, subplot) {
  function drawGrid (line 41984) | function drawGrid(plotinfo, counteraxis, subplot) {
  function makeAxisGroups (line 42102) | function makeAxisGroups(gd, traces) {
  function mergeAxisGroups (line 42141) | function mergeAxisGroups(intoSet, fromSet) {
  function swapAxisGroup (line 42147) | function swapAxisGroup(gd, xIds, yIds) {
  function swapAxisAttrs (line 42211) | function swapAxisAttrs(layout, key, xFullAxes, yFullAxes) {
  function linearOK (line 42263) | function linearOK(array) {
  function moreDates (line 42278) | function moreDates(a, calendar) {
  function category (line 42296) | function category(a) {
  function coerce2 (line 42358) | function coerce2(attr, dflt) {
  function listNames (line 42482) | function listNames(gd, axLetter, only2d) {
  function getConstraintOpts (line 42732) | function getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutO...
  function updateConstraintGroups (line 42781) | function updateConstraintGroups(constraintGroups, thisGroup, thisID, sca...
  function updateDomain (line 43015) | function updateDomain(ax, factor) {
  function recomputeAxisLists (line 43098) | function recomputeAxisLists() {
  function zoomPrep (line 43217) | function zoomPrep(e, startX, startY) {
  function zoomMove (line 43236) | function zoomMove(dx0, dy0) {
  function zoomDone (line 43307) | function zoomDone(dragged, numClicks) {
  function dragDone (line 43327) | function dragDone(dragged, numClicks) {
  function zoomWheel (line 43376) | function zoomWheel(e) {
  function plotDrag (line 43471) | function plotDrag(dx, dy) {
  function ticksAndAnnotations (line 43560) | function ticksAndAnnotations(ns, ew) {
  function doubleClick (line 43609) | function doubleClick() {
  function dragTail (line 43679) | function dragTail(zoommode) {
  function updateSubplots (line 43698) | function updateSubplots(viewBox) {
  function makeDragger (line 43798) | function makeDragger(plotinfo, dragClass, cursor, x, y, w, h) {
  function isDirectionActive (line 43813) | function isDirectionActive(axList, activeVal) {
  function getEndText (line 43820) | function getEndText(ax, end) {
  function zoomAxRanges (line 43841) | function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxe...
  function dragAxList (line 43869) | function dragAxList(axList, pix) {
  function dZoom (line 43886) | function dZoom(d) {
  function getDragCursor (line 43891) | function getDragCursor(nsew, dragmode) {
  function makeZoombox (line 43900) | function makeZoombox(zoomlayer, lum, xs, ys, path0) {
  function makeCorners (line 43911) | function makeCorners(zoomlayer, xs, ys) {
  function clearSelect (line 43924) | function clearSelect(zoomlayer) {
  function updateZoombox (line 43931) | function updateZoombox(zb, corners, box, path0, dimmed, lum) {
  function removeZoombox (line 43946) | function removeZoombox(gd) {
  function isSelectOrLasso (line 43952) | function isSelectOrLasso(dragmode) {
  function xCorners (line 43958) | function xCorners(box, y0) {
  function yCorners (line 43966) | function yCorners(box, x0) {
  function xyCorners (line 43974) | function xyCorners(box) {
  function calcLinks (line 43987) | function calcLinks(constraintGroups, xIDs, yIDs) {
  function plotOne (line 44298) | function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnComplete...
  function makeSubplotData (line 44435) | function makeSubplotData(gd) {
  function makeSubplotLayer (line 44468) | function makeSubplotLayer(plotinfo) {
  function purgeSubplotLayers (line 44558) | function purgeSubplotLayers(layers, fullLayout) {
  function joinLayer (line 44596) | function joinLayer(parent, nodeType, className) {
  function axSort (line 45137) | function axSort(a, b) {
  function coerce (line 45158) | function coerce(attr, dflt) {
  function getCounterAxes (line 45162) | function getCounterAxes(axLetter) {
  function getOverlayableAxes (line 45169) | function getOverlayableAxes(axLetter, axName) {
  function flattenUniqueSort (line 45307) | function flattenUniqueSort(axisLetter, sortFunction, data) {
  function getAxId (line 45489) | function getAxId(ax) { return ax._id; }
  function axValue (line 45570) | function axValue(ax) {
  function ascending (line 45575) | function ascending(a, b) { return a - b; }
  function fillSelectionItem (line 45683) | function fillSelectionItem(selection, searchInfo) {
  function fromLog (line 45728) | function fromLog(v) {
  function toLog (line 45768) | function toLog(v, clip) {
  function dt2ms (line 45788) | function dt2ms(v, _, calendar) {
  function ms2dt (line 45802) | function ms2dt(v, r, calendar) {
  function getCategoryName (line 45806) | function getCategoryName(v) {
  function setCategoryIndex (line 45823) | function setCategoryIndex(v) {
  function getCategoryIndex (line 45843) | function getCategoryIndex(v) {
  function l2p (line 45854) | function l2p(v) {
  function p2l (line 45861) | function p2l(px) { return (px - ax._b) / ax._m; }
  function getShowAttrDflt (line 46233) | function getShowAttrDflt(containerIn) {
  function computeUpdates (line 46390) | function computeUpdates(layout) {
  function computeAffectedSubplots (line 46424) | function computeAffectedSubplots(fullLayout, updatedAxisIds, updates) {
  function updateLayoutObjs (line 46469) | function updateLayoutObjs() {
  function ticksAndAnnotations (line 46489) | function ticksAndAnnotations(xa, ya) {
  function unsetSubplotTransform (line 46518) | function unsetSubplotTransform(subplot) {
  function updateSubplot (line 46544) | function updateSubplot(subplot, progress) {
  function transitionComplete (line 46630) | function transitionComplete() {
  function transitionInterrupt (line 46651) | function transitionInterrupt() {
  function doFrame (line 46677) | function doFrame() {
  function setAutoType (line 46745) | function setAutoType(ax, data) {
  function getFirstNonEmptyTrace (line 46796) | function getFirstNonEmptyTrace(data, id, axLetter) {
  function getBoxPosLetter (line 46811) | function getBoxPosLetter(trace) {
  function isBoxWithoutPositionCoords (line 46815) | function isBoxWithoutPositionCoords(trace, axLetter) {
  function bindingValueHasChanged (line 47049) | function bindingValueHasChanged(gd, binding, cache) {
  function computeAnimateBindings (line 47140) | function computeAnimateBindings(gd, args) {
  function computeLayoutBindings (line 47150) | function computeLayoutBindings(gd, args) {
  function computeDataBindings (line 47170) | function computeDataBindings(gd, args) {
  function crawl (line 47239) | function crawl(attrs, callback, path, depth) {
  function xformMatrix (line 47345) | function xformMatrix(m, v) {
  function project (line 47358) | function project(camera, v) {
  function positionPlayWithData (line 47872) | function positionPlayWithData(gd, container) {
  function remapTransformedArrays (line 48070) | function remapTransformedArrays(cd0, newTrace) {
  function locateColorAttrs (line 48292) | function locateColorAttrs(attr, attrName, attrs, level) {
  function pushModule (line 48332) | function pushModule(fullTrace) {
  function coerce (line 48419) | function coerce(attr, dflt) {
  function coerce (line 48451) | function coerce(attr, dflt) {
  function coerce (line 48464) | function coerce(attr, dflt) {
  function coerce (line 48477) | function coerce(attr, dflt) {
  function coerce (line 48495) | function coerce(attr, dflt) {
  function coerceSubplotAttr (line 48499) | function coerceSubplotAttr(subplotType, subplotAttr) {
  function applyTransforms (line 48603) | function applyTransforms(fullTrace, fullData, layout, fullLayout) {
  function coerce (line 48627) | function coerce(attr, dflt) {
  function calculateReservedMargins (line 48756) | function calculateReservedMargins(margins) {
  function stripObj (line 49082) | function stripObj(d) {
  function prepareTransitions (line 49439) | function prepareTransitions() {
  function executeCallbacks (line 49494) | function executeCallbacks(list) {
  function flushCallbacks (line 49503) | function flushCallbacks(list) {
  function executeTransitions (line 49512) | function executeTransitions() {
  function completeTransition (line 49599) | function completeTransition(callback) {
  function interruptPreviousTransitions (line 49621) | function interruptPreviousTransitions() {
  function initCategories (line 49779) | function initCategories(axList) {
  function filterVisible (line 49802) | function filterVisible(calcDataIn) {
  function mergeAttrs (line 49905) | function mergeAttrs(axisName, nonCommonAttrs) {
  function render (line 50039) | function render(_container) {
  function exports (line 50796) | function exports() {
  function exports (line 51058) | function exports() {
  function exports (line 51463) | function exports(_inputConfig, _container) {
  function execute (line 51541) | function execute(command, action) {
  function findArrayRegexps (line 51696) | function findArrayRegexps(_module) {
  function getTraceType (line 51774) | function getTraceType(traceType) {
  function cloneLayoutOverride (line 51798) | function cloneLayoutOverride(tileClass) {
  function keyIsAxis (line 51834) | function keyIsAxis(keyName) {
  function downloadImage (line 51975) | function downloadImage(gd, opts) {
  function svgToImg (line 52158) | function svgToImg(opts) {
  function toImage (line 52302) | function toImage(gd, opts) {
  function htmlEntityDecode (line 52377) | function htmlEntityDecode(s) {
  function xmlEntityEncode (line 52389) | function xmlEntityEncode(str) {
  function coerce (line 52777) | function coerce(attr, dflt) {
  function coerce (line 53027) | function coerce(attr, dflt) {
  function roundWithLine (line 53156) | function roundWithLine(v) {
  function expandToVisible (line 53163) | function expandToVisible(v, vc) {
  function appendBarText (line 53206) | function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
  function getTransformToMoveInsideBar (line 53318) | function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB, orientation) {
  function getTransformToMoveOutsideBar (line 53403) | function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientatio...
  function getTransform (line 53463) | function getTransform(textX, textY, targetX, targetY, scale, rotate) {
  function getText (line 53485) | function getText(trace, index) {
  function getTextPosition (line 53490) | function getTextPosition(trace, index) {
  function getTextFont (line 53495) | function getTextFont(trace, index, defaultValue) {
  function getInsideTextFont (line 53500) | function getInsideTextFont(trace, index, defaultValue) {
  function getOutsideTextFont (line 53505) | function getOutsideTextFont(trace, index, defaultValue) {
  function getFontValue (line 53510) | function getFontValue(attributeDefinition, attributeValue, index, defaul...
  function getValue (line 53527) | function getValue(arrayOrScalar, index) {
  function coerceString (line 53534) | function coerceString(attributeDefinition, value, defaultValue) {
  function coerceEnumerated (line 53547) | function coerceEnumerated(attributeDefinition, value, defaultValue) {
  function coerceNumber (line 53557) | function coerceNumber(attributeDefinition, value, defaultValue) {
  function coerceColor (line 53574) | function coerceColor(attributeDefinition, value, defaultValue) {
  function setGroupPositions (line 53639) | function setGroupPositions(gd, pa, sa, calcTraces) {
  function setGroupPositionsInOverlayMode (line 53693) | function setGroupPositionsInOverlayMode(gd, pa, sa, calcTraces) {
  function setGroupPositionsInGroupMode (line 53725) | function setGroupPositionsInGroupMode(gd, pa, sa, calcTraces) {
  function setGroupPositionsInStackOrRelativeMode (line 53748) | function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) {
  function setOffsetAndWidth (line 53786) | function setOffsetAndWidth(gd, pa, sieve) {
  function setOffsetAndWidthInGroupMode (line 53828) | function setOffsetAndWidthInGroupMode(gd, pa, sieve) {
  function applyAttributes (line 53878) | function applyAttributes(sieve) {
  function setBarCenterAndWidth (line 53962) | function setBarCenterAndWidth(gd, pa, sieve) {
  function updatePositionAxis (line 53989) | function updatePositionAxis(gd, pa, sieve, allowMinDtick) {
  function expandRange (line 54038) | function expandRange(range, newValue) {
  function setBaseAndTop (line 54046) | function setBaseAndTop(gd, sa, sieve) {
  function stackBars (line 54074) | function stackBars(gd, sa, sieve) {
  function sieveBars (line 54113) | function sieveBars(gd, sa, sieve) {
  function normalizeBars (line 54128) | function normalizeBars(gd, sa, sieve) {
  function getAxisLetter (line 54178) | function getAxisLetter(ax) {
  function Sieve (line 54210) | function Sieve(traces, separateNegativeValues, dontMergeOverlappingData) {
  function getCdModule (line 54621) | function getCdModule(calcdata, _module) {
  function nextDefaultColor (line 54768) | function nextDefaultColor(index) {
  function coerce (line 54803) | function coerce(attr, dflt) {
  function coerce (line 54968) | function coerce(attr, dflt) {
  function handleMouseOver (line 55066) | function handleMouseOver(evt) {
  function handleMouseOut (line 55128) | function handleMouseOut(evt) {
  function handleClick (line 55141) | function handleClick() {
  function arc (line 55169) | function arc(start, finish, cw, scale) {
  function transformInsideText (line 55344) | function transformInsideText(textBB, pt, cd0) {
  function getInscribedRadiusFraction (line 55400) | function getInscribedRadiusFraction(pt, cd0) {
  function transformOutsideText (line 55407) | function transformOutsideText(textBB, pt) {
  function scootLabels (line 55426) | function scootLabels(quadrants, trace) {
  function scalePies (line 55540) | function scalePies(cdpie, plotSize) {
  function setCoords (line 55605) | function setCoords(cd) {
  function maxExtent (line 55678) | function maxExtent(tilt, tiltAxisFraction, depth) {
  function coerce (line 56375) | function coerce(attr, dflt) {
  function getPt (line 56826) | function getPt(index) {
  function getTolerance (line 56834) | function getTolerance(pt) {
  function ptDist (line 56840) | function ptDist(pt1, pt2) {
  function createFills (line 57223) | function createFills(gd, scatterlayer, plotinfo) {
  function plotOne (line 57266) | function plotOne(gd, idx, plotinfo, cdscatter, cdscatterAll, element, tr...
  function selectMarkers (line 57656) | function selectMarkers(gd, idx, plotinfo, cdscatter, cdscatterAll) {
Condensed preview — 253 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,698K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1033,
    "preview": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n   "
  },
  {
    "path": ".gitignore",
    "chars": 37,
    "preview": ".bundle/\nlog/*.log\npkg/\nGemfile.lock\n"
  },
  {
    "path": ".rspec",
    "chars": 30,
    "preview": "--color\n--require spec_helper\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 9643,
    "preview": "## v2.10.0 (2025-07-22)\n### Changes\n- Update Rails to 7.0\n- Use `Bundler.with_unbundled_env` instead of deprecated `Bund"
  },
  {
    "path": "Gemfile",
    "chars": 913,
    "preview": "source 'https://rubygems.org'\n\n# Declare your gem's dependencies in barbeque.gemspec.\n# Bundler will treat runtime depen"
  },
  {
    "path": "MIT-LICENSE",
    "chars": 1055,
    "preview": "Copyright 2016 Takashi Kokubun\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this sof"
  },
  {
    "path": "README.md",
    "chars": 3252,
    "preview": "# Barbeque [![Build Status](https://github.com/cookpad/barbeque/actions/workflows/ci.yml/badge.svg?branch=master)](https"
  },
  {
    "path": "Rakefile",
    "chars": 1975,
    "preview": "begin\n  require 'bundler/setup'\nrescue LoadError\n  puts 'You must `gem install bundler` and `bundle install` to run rake"
  },
  {
    "path": "app/assets/config/barbeque_manifest.js",
    "chars": 95,
    "preview": "//= link_directory ../javascripts/barbeque .js\n//= link_directory ../stylesheets/barbeque .css\n"
  },
  {
    "path": "app/assets/images/barbeque/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "app/assets/javascripts/barbeque/application.js",
    "chars": 780,
    "preview": "// This is a manifest file that'll be compiled into application.js, which will include all the files\n// listed below.\n//"
  },
  {
    "path": "app/assets/javascripts/barbeque/job_definitions.js",
    "chars": 657,
    "preview": "jQuery(function($) {\n  if (!document.querySelector('.barbeque_job_definitions_controller')) {\n    return;\n  }\n\n  $('.use"
  },
  {
    "path": "app/assets/javascripts/barbeque/job_queues.js",
    "chars": 4120,
    "preview": "jQuery(function($) {\n  if (!document.querySelector('.barbeque_job_queues_controller')) {\n    return;\n  }\n  const sqsDiv "
  },
  {
    "path": "app/assets/stylesheets/barbeque/application.css",
    "chars": 469,
    "preview": "/*\n *= require bootstrap\n *= require AdminLTE\n *= require skins/skin-blue\n\n *= require barbeque/job_definitions\n */\n\n:ro"
  },
  {
    "path": "app/assets/stylesheets/barbeque/job_definitions.css",
    "chars": 1114,
    "preview": ".barbeque_job_definitions_controller .use_slack_notification_wrapper label {\n  font-weight: normal;\n}\n\n.barbeque_job_def"
  },
  {
    "path": "app/controllers/barbeque/api/application_controller.rb",
    "chars": 906,
    "preview": "require 'garage'\n\nclass Barbeque::Api::ApplicationController < ActionController::API\n  before_action :force_json_format\n"
  },
  {
    "path": "app/controllers/barbeque/api/job_executions_controller.rb",
    "chars": 2085,
    "preview": "require 'barbeque/maintenance'\n\nclass Barbeque::Api::JobExecutionsController < Barbeque::Api::ApplicationController\n  in"
  },
  {
    "path": "app/controllers/barbeque/api/job_retries_controller.rb",
    "chars": 738,
    "preview": "class Barbeque::Api::JobRetriesController < Barbeque::Api::ApplicationController\n  include Garage::RestfulActions\n\n  # h"
  },
  {
    "path": "app/controllers/barbeque/api/revision_locks_controller.rb",
    "chars": 920,
    "preview": "class Barbeque::Api::RevisionLocksController < Barbeque::Api::ApplicationController\n  include Garage::RestfulActions\n\n  "
  },
  {
    "path": "app/controllers/barbeque/application_controller.rb",
    "chars": 123,
    "preview": "module Barbeque\n  class ApplicationController < ActionController::Base\n    protect_from_forgery with: :exception\n  end\ne"
  },
  {
    "path": "app/controllers/barbeque/apps_controller.rb",
    "chars": 954,
    "preview": "class Barbeque::AppsController < Barbeque::ApplicationController\n  def index\n    @apps = Barbeque::App.all\n  end\n\n  def "
  },
  {
    "path": "app/controllers/barbeque/job_definitions_controller.rb",
    "chars": 3141,
    "preview": "class Barbeque::JobDefinitionsController < Barbeque::ApplicationController\n  def index\n    @job_definitions = Barbeque::"
  },
  {
    "path": "app/controllers/barbeque/job_executions_controller.rb",
    "chars": 1123,
    "preview": "class Barbeque::JobExecutionsController < Barbeque::ApplicationController\n  ID_REGEXP = /\\A[0-9]+\\z/\n\n  def show\n    if "
  },
  {
    "path": "app/controllers/barbeque/job_queues_controller.rb",
    "chars": 4741,
    "preview": "require 'aws-sdk-cloudwatch'\nrequire 'aws-sdk-sqs'\nrequire 'barbeque/config'\n\nclass Barbeque::JobQueuesController < Barb"
  },
  {
    "path": "app/controllers/barbeque/job_retries_controller.rb",
    "chars": 606,
    "preview": "class Barbeque::JobRetriesController < Barbeque::ApplicationController\n  def show\n    @job_retry = Barbeque::JobRetry.fi"
  },
  {
    "path": "app/controllers/barbeque/monitors_controller.rb",
    "chars": 1663,
    "preview": "class Barbeque::MonitorsController < Barbeque::ApplicationController\n  def index\n    now = Time.zone.now\n    from = 6.ho"
  },
  {
    "path": "app/controllers/barbeque/sns_subscriptions_controller.rb",
    "chars": 1320,
    "preview": "class Barbeque::SnsSubscriptionsController < Barbeque::ApplicationController\n  def index\n    @sns_subscriptions = Barbeq"
  },
  {
    "path": "app/helpers/barbeque/application_helper.rb",
    "chars": 53,
    "preview": "module Barbeque\n  module ApplicationHelper\n  end\nend\n"
  },
  {
    "path": "app/helpers/barbeque/job_definitions_helper.rb",
    "chars": 336,
    "preview": "module Barbeque::JobDefinitionsHelper\n  def distance_of_time(from, to)\n    return '' if from.nil? || to.nil?\n\n    secs  "
  },
  {
    "path": "app/helpers/barbeque/job_executions_helper.rb",
    "chars": 435,
    "preview": "module Barbeque::JobExecutionsHelper\n  def status_label(status)\n    color =\n      case status\n      when 'success'\n     "
  },
  {
    "path": "app/models/barbeque/api/application_resource.rb",
    "chars": 202,
    "preview": "require 'garage'\n\nclass Barbeque::Api::ApplicationResource\n  include Garage::Representer\n  include Garage::Authorizable\n"
  },
  {
    "path": "app/models/barbeque/api/database_maintenance_resource.rb",
    "chars": 204,
    "preview": "class Barbeque::Api::DatabaseMaintenanceResource\n  include Garage::Representer\n\n  property :message\n\n  delegate :message"
  },
  {
    "path": "app/models/barbeque/api/job_execution_resource.rb",
    "chars": 519,
    "preview": "class Barbeque::Api::JobExecutionResource < Barbeque::Api::ApplicationResource\n  property :message_id\n  property :status"
  },
  {
    "path": "app/models/barbeque/api/job_retry_resource.rb",
    "chars": 167,
    "preview": "class Barbeque::Api::JobRetryResource < Barbeque::Api::ApplicationResource\n  property :message_id\n\n  property :status\n\n "
  },
  {
    "path": "app/models/barbeque/api/revision_lock_resource.rb",
    "chars": 181,
    "preview": "class Barbeque::Api::RevisionLockResource < Barbeque::Api::ApplicationResource\n  property :revision\n\n  def revision\n    "
  },
  {
    "path": "app/models/barbeque/app.rb",
    "chars": 221,
    "preview": "class Barbeque::App < Barbeque::ApplicationRecord\n  validates :name, presence: true, uniqueness: true\n  validates :docke"
  },
  {
    "path": "app/models/barbeque/application_record.rb",
    "chars": 104,
    "preview": "module Barbeque\n  class ApplicationRecord < ActiveRecord::Base\n    self.abstract_class = true\n  end\nend\n"
  },
  {
    "path": "app/models/barbeque/docker_container.rb",
    "chars": 66,
    "preview": "class Barbeque::DockerContainer < Barbeque::ApplicationRecord\nend\n"
  },
  {
    "path": "app/models/barbeque/ecs_hako_task.rb",
    "chars": 62,
    "preview": "class Barbeque::EcsHakoTask < Barbeque::ApplicationRecord\nend\n"
  },
  {
    "path": "app/models/barbeque/job_definition.rb",
    "chars": 1239,
    "preview": "class Barbeque::JobDefinition < Barbeque::ApplicationRecord\n  belongs_to :app\n  has_many :job_executions\n  has_many :sns"
  },
  {
    "path": "app/models/barbeque/job_execution.rb",
    "chars": 1071,
    "preview": "class Barbeque::JobExecution < Barbeque::ApplicationRecord\n  belongs_to :job_definition\n  belongs_to :job_queue\n  has_on"
  },
  {
    "path": "app/models/barbeque/job_queue.rb",
    "chars": 1462,
    "preview": "require 'barbeque/maintenance'\n\nclass Barbeque::JobQueue < Barbeque::ApplicationRecord\n  SQS_NAME_PREFIX = ENV['BARBEQUE"
  },
  {
    "path": "app/models/barbeque/job_retry.rb",
    "chars": 587,
    "preview": "class Barbeque::JobRetry < Barbeque::ApplicationRecord\n  belongs_to :job_execution\n  has_one :job_definition, through: :"
  },
  {
    "path": "app/models/barbeque/retry_config.rb",
    "chars": 760,
    "preview": "class Barbeque::RetryConfig < Barbeque::ApplicationRecord\n  belongs_to :job_definition\n\n  validates :retry_limit, numeri"
  },
  {
    "path": "app/models/barbeque/slack_notification.rb",
    "chars": 151,
    "preview": "class Barbeque::SlackNotification < Barbeque::ApplicationRecord\n  belongs_to :job_definition, optional: true\n\n  validate"
  },
  {
    "path": "app/models/barbeque/sns_subscription.rb",
    "chars": 577,
    "preview": "module Barbeque\n  class SnsSubscription < ApplicationRecord\n    belongs_to :job_queue\n    belongs_to :job_definition\n   "
  },
  {
    "path": "app/models/concerns/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "app/services/barbeque/message_enqueuing_service.rb",
    "chars": 1585,
    "preview": "require 'aws-sdk-sqs'\n\nclass Barbeque::MessageEnqueuingService\n  DEFAULT_QUEUE = ENV['BARBEQUE_DEFAULT_QUEUE'] || 'defau"
  },
  {
    "path": "app/services/barbeque/message_retrying_service.rb",
    "chars": 731,
    "preview": "require 'aws-sdk-sqs'\n\nclass Barbeque::MessageRetryingService\n  DEFAULT_DELAY_SECONDS = 0\n\n  def self.sqs_client\n    @sq"
  },
  {
    "path": "app/services/barbeque/sns_subscription_service.rb",
    "chars": 3774,
    "preview": "require 'aws-sdk-sns'\nrequire 'aws-sdk-sqs'\n\nclass Barbeque::SnsSubscriptionService\n  def self.sqs_client\n    @sqs_clien"
  },
  {
    "path": "app/views/barbeque/apps/_form.html.haml",
    "chars": 975,
    "preview": ".box.box-primary\n  .box-header\n    %h3.box-title.with_padding\n      #{action_name.capitalize} Application\n\n  .box-body\n "
  },
  {
    "path": "app/views/barbeque/apps/edit.html.haml",
    "chars": 234,
    "preview": "- content_for(:title, \"Edit #{@app.name} - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home'"
  },
  {
    "path": "app/views/barbeque/apps/index.html.haml",
    "chars": 672,
    "preview": "- content_for(:title, 'Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li.active Home\n\n.box.box-primary\n  .bo"
  },
  {
    "path": "app/views/barbeque/apps/new.html.haml",
    "chars": 177,
    "preview": "- content_for(:title, 'New application - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', "
  },
  {
    "path": "app/views/barbeque/apps/show.html.haml",
    "chars": 1420,
    "preview": "- content_for(:title, \"#{@app.name} - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', roo"
  },
  {
    "path": "app/views/barbeque/job_definitions/_form.html.haml",
    "chars": 1604,
    "preview": ".box.box-primary\n  .box-header\n    %h3.box-title.with_padding\n      #{action_name.capitalize} Job Definition\n\n  .box-bod"
  },
  {
    "path": "app/views/barbeque/job_definitions/_retry_configuration_field.html.haml",
    "chars": 1220,
    "preview": "= fields_for(job_definition) do |job_definition_f|\n  = job_definition_f.fields_for(:retry_config) do |f|\n    = f.hidden_"
  },
  {
    "path": "app/views/barbeque/job_definitions/_slack_notification_field.html.haml",
    "chars": 1419,
    "preview": "= fields_for job_definition do |job_definition_f|\n  = job_definition_f.fields_for :slack_notification do |f|\n    = f.hid"
  },
  {
    "path": "app/views/barbeque/job_definitions/edit.html.haml",
    "chars": 387,
    "preview": "- content_for(:title, \"Edit job definition #{@job_definition.job} - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrum"
  },
  {
    "path": "app/views/barbeque/job_definitions/index.html.haml",
    "chars": 823,
    "preview": "- content_for(:title, 'Job definitions - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', "
  },
  {
    "path": "app/views/barbeque/job_definitions/new.html.haml",
    "chars": 299,
    "preview": "- content_for(:title, 'New job definition - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home"
  },
  {
    "path": "app/views/barbeque/job_definitions/show.html.haml",
    "chars": 4478,
    "preview": "- content_for(:title, \"#{@job_definition.job} - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('"
  },
  {
    "path": "app/views/barbeque/job_definitions/stats.html.haml",
    "chars": 1895,
    "preview": "- content_for(:title, \"#{@job_definition.job} statistics - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li"
  },
  {
    "path": "app/views/barbeque/job_executions/show.html.haml",
    "chars": 3384,
    "preview": "- content_for(:title, \"Job execution ##{@job_execution.id} of #{@job_definition.job} - Barbeque\")\n- content_for(:header)"
  },
  {
    "path": "app/views/barbeque/job_queues/_form.html.haml",
    "chars": 876,
    "preview": ".box.box-primary\n  .box-header\n    %h3.box-title.with_padding\n      #{action_name.capitalize} Job Queue\n\n  .box-body\n   "
  },
  {
    "path": "app/views/barbeque/job_queues/edit.html.haml",
    "chars": 328,
    "preview": "- content_for(:title, \"Edit #{@job_queue.name} job queue - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li"
  },
  {
    "path": "app/views/barbeque/job_queues/index.html.haml",
    "chars": 698,
    "preview": "- content_for(:title, 'Job queues - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', root_"
  },
  {
    "path": "app/views/barbeque/job_queues/new.html.haml",
    "chars": 229,
    "preview": "- content_for(:title, 'New job queue - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', ro"
  },
  {
    "path": "app/views/barbeque/job_queues/show.html.haml",
    "chars": 968,
    "preview": "- content_for(:title, \"#{@job_queue.name} job queue - Barbeque\")\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= lin"
  },
  {
    "path": "app/views/barbeque/job_retries/show.html.haml",
    "chars": 2220,
    "preview": "- content_for(:title, \"Job retry ##{@job_retry.id} of #{@job_definition.job} - Barbeque\")\n- content_for(:header) do\n  %o"
  },
  {
    "path": "app/views/barbeque/monitors/index.html.haml",
    "chars": 1951,
    "preview": "- content_for(:title, 'Monitors - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home', root_pa"
  },
  {
    "path": "app/views/barbeque/sns_subscriptions/_form.html.haml",
    "chars": 1493,
    "preview": ".box.box-primary\n  .box-header\n    %h3.box-title.with_padding\n      #{action_name.capitalize} SNS Subscription\n\n  .box-b"
  },
  {
    "path": "app/views/barbeque/sns_subscriptions/edit.html.haml",
    "chars": 446,
    "preview": "- content_for(:title, \"Edit SNS subscription between #{@sns_subscription.topic_arn} and #{@sns_subscription.job_queue.na"
  },
  {
    "path": "app/views/barbeque/sns_subscriptions/index.html.haml",
    "chars": 862,
    "preview": "- content_for(:title, 'SNS subscriptions - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Home'"
  },
  {
    "path": "app/views/barbeque/sns_subscriptions/new.html.haml",
    "chars": 257,
    "preview": "- content_for(:title, 'New SNS subscription - Barbeque')\n- content_for(:header) do\n  %ol.breadcrumb\n    %li= link_to('Ho"
  },
  {
    "path": "app/views/barbeque/sns_subscriptions/show.html.haml",
    "chars": 1245,
    "preview": "- content_for(:title, \"SNS subscription between #{@sns_subscription.topic_arn} and #{@sns_subscription.job_queue.name} -"
  },
  {
    "path": "app/views/layouts/barbeque/_header.html.haml",
    "chars": 286,
    "preview": "%header.main-header\n  %a.logo{ href: '/' }\n    %span.logo-mini\n      %b> BBQ\n    %span.logo-lg\n      %b> Barbeque\n\n  %na"
  },
  {
    "path": "app/views/layouts/barbeque/_sidebar.html.haml",
    "chars": 933,
    "preview": "%aside.main-sidebar\n  %section.sidebar\n    %ul.sidebar-menu\n      %li.header Menu\n      %li{ class: ('active' if control"
  },
  {
    "path": "app/views/layouts/barbeque/application.html.haml",
    "chars": 1376,
    "preview": "!!!\n%html\n  %head\n    %meta{ content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type' }\n    %meta{ charset: 'UT"
  },
  {
    "path": "app/views/layouts/barbeque/apps.html.haml",
    "chars": 117,
    "preview": "- content_for(:header) do\n  %h1\n    .fa.fa-users\n    Applications\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/job_definitions.html.haml",
    "chars": 120,
    "preview": "- content_for(:header) do\n  %h1\n    .fa.fa-tasks\n    Job Definitions\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/job_executions.html.haml",
    "chars": 102,
    "preview": "- content_for(:header) do\n  %h1\n    Job Executions\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/job_queues.html.haml",
    "chars": 111,
    "preview": "- content_for(:header) do\n  %h1\n    .fa.fa-cubes\n    Queues\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/job_retries.html.haml",
    "chars": 99,
    "preview": "- content_for(:header) do\n  %h1\n    Job Retries\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/monitors.html.haml",
    "chars": 118,
    "preview": "- content_for(:header) do\n  %h1\n    .fa.fa-tachometer\n    Monitors\n\n= render template: 'layouts/barbeque/application'\n"
  },
  {
    "path": "app/views/layouts/barbeque/sns_subscriptions.html.haml",
    "chars": 134,
    "preview": "- content_for(:header) do\n  %h1\n    %i.fa.fa-calendar-plus-o\n    SNS Subscriptions\n\n= render template: 'layouts/barbeque"
  },
  {
    "path": "barbeque.gemspec",
    "chars": 1334,
    "preview": "$:.push File.expand_path(\"../lib\", __FILE__)\nrequire \"barbeque/version\"\n\nGem::Specification.new do |s|\n  s.name        ="
  },
  {
    "path": "bin/rails",
    "chars": 489,
    "preview": "#!/usr/bin/env ruby\n# This command will automatically be run when you run \"rails\" with Rails gems\n# installed from the r"
  },
  {
    "path": "compose.yaml",
    "chars": 213,
    "preview": "services:\n  db:\n    image: mysql:8.0\n    ports:\n      - 3306:3306\n    environment:\n      MYSQL_ROOT_PASSWORD: barbeque_r"
  },
  {
    "path": "config/initializers/garage.rb",
    "chars": 105,
    "preview": "require 'garage'\n\nGarage.configure {}\nGarage.configuration.strategy = Garage::Strategy::NoAuthentication\n"
  },
  {
    "path": "config/routes.rb",
    "chars": 1100,
    "preview": "Barbeque::Engine.routes.draw do\n  root to: 'apps#index'\n\n  resources :apps, except: :index\n\n  resources :job_definitions"
  },
  {
    "path": "db/migrate/20160217020910_create_job_queues.rb",
    "chars": 357,
    "preview": "class CreateJobQueues < ActiveRecord::Migration[5.0]\n  def change\n    create_table :job_queues, options: 'ENGINE=InnoDB "
  },
  {
    "path": "db/migrate/20160219010912_create_job_executions.rb",
    "chars": 365,
    "preview": "class CreateJobExecutions < ActiveRecord::Migration[5.0]\n  def change\n    create_table :job_executions, options: 'ENGINE"
  },
  {
    "path": "db/migrate/20160223060807_create_apps.rb",
    "chars": 343,
    "preview": "class CreateApps < ActiveRecord::Migration[5.0]\n  def change\n    create_table :apps, options: 'ENGINE=InnoDB ROW_FORMAT="
  },
  {
    "path": "db/migrate/20160223183348_create_job_definitions.rb",
    "chars": 414,
    "preview": "class CreateJobDefinitions < ActiveRecord::Migration[5.0]\n  def change\n    create_table :job_definitions, options: 'ENGI"
  },
  {
    "path": "db/migrate/20160225020801_add_job_definition_id_to_job_executions.rb",
    "chars": 207,
    "preview": "class AddJobDefinitionIdToJobExecutions < ActiveRecord::Migration[5.0]\n  def change\n    add_column :job_executions, :job"
  },
  {
    "path": "db/migrate/20160412083604_add_finished_at_to_job_executions.rb",
    "chars": 145,
    "preview": "class AddFinishedAtToJobExecutions < ActiveRecord::Migration[5.0]\n  def change\n    add_column :job_executions, :finished"
  },
  {
    "path": "db/migrate/20160415043427_create_slack_notifications.rb",
    "chars": 402,
    "preview": "class CreateSlackNotifications < ActiveRecord::Migration[5.0]\n  def change\n    create_table :slack_notifications, option"
  },
  {
    "path": "db/migrate/20160509041452_add_job_queue_id_to_job_executions.rb",
    "chars": 145,
    "preview": "class AddJobQueueIdToJobExecutions < ActiveRecord::Migration[5.0]\n  def change\n    add_column :job_executions, :job_queu"
  },
  {
    "path": "db/migrate/20160516041710_create_job_retries.rb",
    "chars": 433,
    "preview": "class CreateJobRetries < ActiveRecord::Migration[5.0]\n  def change\n    create_table :job_retries, options: 'ENGINE=InnoD"
  },
  {
    "path": "db/migrate/20160829023237_prefix_barbeque_to_tables.rb",
    "chars": 415,
    "preview": "class PrefixBarbequeToTables < ActiveRecord::Migration[5.0]\n  def change\n    rename_table :apps, :barbeque_apps\n    rena"
  },
  {
    "path": "db/migrate/20170420030157_create_barbeque_sns_subscriptions.rb",
    "chars": 301,
    "preview": "class CreateBarbequeSnsSubscriptions < ActiveRecord::Migration[5.0]\n  def change\n    create_table :barbeque_sns_subscrip"
  },
  {
    "path": "db/migrate/20170711085157_create_barbeque_docker_containers.rb",
    "chars": 366,
    "preview": "class CreateBarbequeDockerContainers < ActiveRecord::Migration[5.0]\n  def change\n    create_table :barbeque_docker_conta"
  },
  {
    "path": "db/migrate/20170712075449_create_barbeque_ecs_hako_tasks.rb",
    "chars": 392,
    "preview": "class CreateBarbequeEcsHakoTasks < ActiveRecord::Migration[5.0]\n  def change\n    create_table :barbeque_ecs_hako_tasks, "
  },
  {
    "path": "db/migrate/20170724025542_add_index_to_job_execution_status.rb",
    "chars": 182,
    "preview": "class AddIndexToJobExecutionStatus < ActiveRecord::Migration[5.0]\n  def change\n    add_index :barbeque_job_executions, :"
  },
  {
    "path": "db/migrate/20180411070937_add_index_to_barbeque_job_executions_created_at.rb",
    "chars": 153,
    "preview": "class AddIndexToBarbequeJobExecutionsCreatedAt < ActiveRecord::Migration[5.1]\n  def change\n    add_index :barbeque_job_e"
  },
  {
    "path": "db/migrate/20190221050714_create_barbeque_retry_configs.rb",
    "chars": 517,
    "preview": "class CreateBarbequeRetryConfigs < ActiveRecord::Migration[5.2]\n  def change\n    create_table :barbeque_retry_configs, o"
  },
  {
    "path": "db/migrate/20190311034445_add_notify_failure_only_if_retry_limit_reached_to_barbeque_slack_notifications.rb",
    "chars": 257,
    "preview": "class AddNotifyFailureOnlyIfRetryLimitReachedToBarbequeSlackNotifications < ActiveRecord::Migration[5.2]\n  def change\n  "
  },
  {
    "path": "db/migrate/20190315052951_change_barbeque_retry_configs_base_delay_default_value.rb",
    "chars": 257,
    "preview": "class ChangeBarbequeRetryConfigsBaseDelayDefaultValue < ActiveRecord::Migration[5.2]\n  def up\n    change_column_default "
  },
  {
    "path": "db/migrate/20191029105530_change_job_name_to_case_sensitive.rb",
    "chars": 263,
    "preview": "class ChangeJobNameToCaseSensitive < ActiveRecord::Migration[5.2]\n  def up\n    change_column :barbeque_job_definitions, "
  },
  {
    "path": "db/migrate/20240415080757_fix_collations.rb",
    "chars": 2220,
    "preview": "# In MySQL 8.0, the default collation for utf8mb4 is changed to utf8mb4_0900_ai_ci.\n# The column collations are utf8mb4_"
  },
  {
    "path": "doc/api/job_executions.md",
    "chars": 4820,
    "preview": "## GET /v1/job_executions/:message_id\nShows a status of a job_execution.\n\n### Example\n\n#### Request\n```\nGET /v1/job_exec"
  },
  {
    "path": "doc/api/job_retries.md",
    "chars": 727,
    "preview": "## POST /v1/job_executions/:job_execution_message_id/retries\nEnqueues a message to retry a specified message.\n\n### Param"
  },
  {
    "path": "doc/api/revision_locks.md",
    "chars": 1098,
    "preview": "## POST /v1/apps/:app_id/revision_lock\nUpdates a tag of docker_image.\n\n### Parameters\n* `revision` string (required) - D"
  },
  {
    "path": "doc/toc.md",
    "chars": 1024,
    "preview": "## Table of Contents\n* [api/job_executions.md](api/job_executions.md)\n * [GET /v1/job_executions/:message_id](api/job_ex"
  },
  {
    "path": "lib/barbeque/config.rb",
    "chars": 1191,
    "preview": "require 'erb'\nrequire 'yaml'\n\nmodule Barbeque\n  class Config\n    attr_accessor :exception_handler, :executor, :executor_"
  },
  {
    "path": "lib/barbeque/docker_image.rb",
    "chars": 585,
    "preview": "module Barbeque\n  class DockerImage\n    DEFAULT_TAG = 'latest'\n\n    def initialize(str)\n      # See: https://github.com/"
  },
  {
    "path": "lib/barbeque/engine.rb",
    "chars": 489,
    "preview": "# Gems used by Barbeque::Engine\nrequire 'rinku'\n\nmodule Barbeque\n  class Engine < ::Rails::Engine\n    isolate_namespace "
  },
  {
    "path": "lib/barbeque/exception_handler.rb",
    "chars": 1965,
    "preview": "require 'barbeque/config'\n\nmodule Barbeque\n  module ExceptionHandler\n    class << self\n      delegate :clear_context, :s"
  },
  {
    "path": "lib/barbeque/execution_log.rb",
    "chars": 2797,
    "preview": "require 'aws-sdk-s3'\nrequire 'active_support'\nrequire 'active_support/core_ext'\nrequire 'barbeque/exception_handler'\n\nmo"
  },
  {
    "path": "lib/barbeque/execution_poller.rb",
    "chars": 870,
    "preview": "require 'barbeque/exception_handler'\nrequire 'barbeque/executor'\n\nmodule Barbeque\n  class ExecutionPoller\n    def initia"
  },
  {
    "path": "lib/barbeque/executor/docker.rb",
    "chars": 5618,
    "preview": "require 'barbeque/docker_image'\nrequire 'barbeque/execution_log'\nrequire 'barbeque/slack_notifier'\nrequire 'open3'\n\nmodu"
  },
  {
    "path": "lib/barbeque/executor/hako.rb",
    "chars": 6587,
    "preview": "require 'barbeque/docker_image'\nrequire 'barbeque/execution_log'\nrequire 'barbeque/hako_s3_client'\nrequire 'barbeque/sla"
  },
  {
    "path": "lib/barbeque/executor.rb",
    "chars": 1103,
    "preview": "require 'barbeque/config'\nrequire 'barbeque/executor/docker'\nrequire 'barbeque/executor/hako'\n\nmodule Barbeque\n  # Execu"
  },
  {
    "path": "lib/barbeque/hako_s3_client.rb",
    "chars": 1276,
    "preview": "require 'uri'\nrequire 'aws-sdk-ecs'\nrequire 'aws-sdk-s3'\n\nmodule Barbeque\n  class HakoS3Client\n    # @param [String] one"
  },
  {
    "path": "lib/barbeque/maintenance.rb",
    "chars": 209,
    "preview": "module Barbeque\n  module Maintenance\n    def self.database_maintenance_mode?\n      ENV['BARBEQUE_DATABASE_MAINTENANCE'] "
  },
  {
    "path": "lib/barbeque/message/base.rb",
    "chars": 1064,
    "preview": "module Barbeque\n  module Message\n    # A model wrapping Aws::SQS::Types::Message.\n    class Base\n      attr_reader :id  "
  },
  {
    "path": "lib/barbeque/message/invalid_message.rb",
    "chars": 157,
    "preview": "require 'barbeque/message/base'\n\nmodule Barbeque\n  module Message\n    class InvalidMessage < Base\n      def valid?\n     "
  },
  {
    "path": "lib/barbeque/message/job_execution.rb",
    "chars": 531,
    "preview": "require 'barbeque/message/base'\n\nmodule Barbeque\n  module Message\n    class JobExecution < Base\n      attr_reader :body "
  },
  {
    "path": "lib/barbeque/message/job_retry.rb",
    "chars": 317,
    "preview": "require 'barbeque/message/base'\n\nmodule Barbeque\n  module Message\n    class JobRetry < Base\n      attr_reader :retry_mes"
  },
  {
    "path": "lib/barbeque/message/notification.rb",
    "chars": 855,
    "preview": "require 'barbeque/message/base'\n\nmodule Barbeque\n  module Message\n    class Notification < Base\n      attr_reader :body "
  },
  {
    "path": "lib/barbeque/message.rb",
    "chars": 870,
    "preview": "require 'barbeque/exception_handler'\nrequire 'barbeque/message/base'\nrequire 'barbeque/message/invalid_message'\nrequire "
  },
  {
    "path": "lib/barbeque/message_handler/job_execution.rb",
    "chars": 2238,
    "preview": "require 'barbeque/execution_log'\nrequire 'barbeque/executor'\nrequire 'barbeque/slack_notifier'\n\nmodule Barbeque\n  module"
  },
  {
    "path": "lib/barbeque/message_handler/job_retry.rb",
    "chars": 2047,
    "preview": "require 'barbeque/execution_log'\nrequire 'barbeque/executor'\nrequire 'barbeque/slack_notifier'\n\nmodule Barbeque\n  module"
  },
  {
    "path": "lib/barbeque/message_handler/notification.rb",
    "chars": 563,
    "preview": "require 'barbeque/message_handler/job_execution'\n\nmodule Barbeque\n  module MessageHandler\n    class Notification < JobEx"
  },
  {
    "path": "lib/barbeque/message_handler.rb",
    "chars": 244,
    "preview": "module Barbeque\n  module MessageHandler\n    class DuplicatedExecution < StandardError; end\n  end\nend\n\nrequire 'barbeque/"
  },
  {
    "path": "lib/barbeque/message_queue.rb",
    "chars": 1342,
    "preview": "require 'aws-sdk-sqs'\nrequire 'barbeque/config'\nrequire 'barbeque/message'\n\nmodule Barbeque\n  class MessageQueue\n    att"
  },
  {
    "path": "lib/barbeque/retry_poller.rb",
    "chars": 938,
    "preview": "require 'barbeque/exception_handler'\nrequire 'barbeque/executor'\n\nmodule Barbeque\n  class RetryPoller\n    def initialize"
  },
  {
    "path": "lib/barbeque/runner.rb",
    "chars": 1625,
    "preview": "require 'barbeque/config'\nrequire 'barbeque/exception_handler'\nrequire 'barbeque/execution_log'\nrequire 'barbeque/messag"
  },
  {
    "path": "lib/barbeque/slack_client.rb",
    "chars": 856,
    "preview": "require 'net/http'\nrequire 'json'\nrequire 'uri'\n\nmodule Barbeque\n  class SlackClient\n    def initialize(channel)\n      @"
  },
  {
    "path": "lib/barbeque/slack_notifier.rb",
    "chars": 3269,
    "preview": "require 'barbeque/slack_client'\n\nmodule Barbeque\n  module SlackNotifier\n    class << self\n      # @param [Barbeque::JobE"
  },
  {
    "path": "lib/barbeque/version.rb",
    "chars": 41,
    "preview": "module Barbeque\n  VERSION = '2.10.0'\nend\n"
  },
  {
    "path": "lib/barbeque/worker.rb",
    "chars": 1649,
    "preview": "require 'barbeque/exception_handler'\nrequire 'barbeque/execution_poller'\nrequire 'barbeque/retry_poller'\nrequire 'barbeq"
  },
  {
    "path": "lib/barbeque.rb",
    "chars": 91,
    "preview": "require 'barbeque/docker_image'\nrequire 'barbeque/engine'\nrequire 'barbeque/execution_log'\n"
  },
  {
    "path": "lib/tasks/barbeque_tasks.rake",
    "chars": 602,
    "preview": "namespace :barbeque do\n  desc 'Start worker to execute jobs'\n  task worker: :environment do\n    ENV['BARBEQUE_QUEUE'] ||"
  },
  {
    "path": "spec/barbeque/config_builder_spec.rb",
    "chars": 681,
    "preview": "require 'rails_helper'\nrequire 'barbeque/config'\n\ndescribe Barbeque::ConfigBuilder do\n  describe '#build_config' do\n    "
  },
  {
    "path": "spec/barbeque/config_spec.rb",
    "chars": 380,
    "preview": "require 'rails_helper'\nrequire 'barbeque/config'\n\ndescribe Barbeque::Config do\n  describe '#executor_options' do\n    it "
  },
  {
    "path": "spec/barbeque/docker_image_spec.rb",
    "chars": 1305,
    "preview": "require 'barbeque'\n\ndescribe Barbeque::DockerImage do\n  it 'parses docker image name without a tag' do\n    image = Barbe"
  },
  {
    "path": "spec/barbeque/exception_handler_spec.rb",
    "chars": 682,
    "preview": "require 'rails_helper'\nrequire 'barbeque/exception_handler'\n\ndescribe Barbeque::ExceptionHandler do\n  describe '.handle_"
  },
  {
    "path": "spec/barbeque/execution_log_spec.rb",
    "chars": 3093,
    "preview": "require 'rails_helper'\nrequire 'barbeque/execution_log'\n\ndescribe Barbeque::ExecutionLog do\n  let(:job_execution) { crea"
  },
  {
    "path": "spec/barbeque/execution_poller_spec.rb",
    "chars": 697,
    "preview": "require 'rails_helper'\nrequire 'barbeque/execution_poller'\n\nRSpec.describe Barbeque::ExecutionPoller do\n  let(:job_queue"
  },
  {
    "path": "spec/barbeque/executor/docker_spec.rb",
    "chars": 22730,
    "preview": "require 'rails_helper'\nrequire 'barbeque/executor/docker'\n\nRSpec.describe Barbeque::Executor::Docker do\n  let(:executor)"
  },
  {
    "path": "spec/barbeque/executor/hako_spec.rb",
    "chars": 21581,
    "preview": "require 'rails_helper'\nrequire 'barbeque/executor/hako'\n\ndescribe Barbeque::Executor::Hako do\n  let(:hako_directory) { '"
  },
  {
    "path": "spec/barbeque/executor_spec.rb",
    "chars": 1132,
    "preview": "require 'rails_helper'\nrequire 'barbeque/executor'\n\ndescribe Barbeque::Executor do\n  describe '.create' do\n    before do"
  },
  {
    "path": "spec/barbeque/message_handler/job_execution_spec.rb",
    "chars": 4632,
    "preview": "require 'rails_helper'\nrequire 'barbeque/worker'\n\ndescribe Barbeque::MessageHandler::JobExecution do\n  describe '#run' d"
  },
  {
    "path": "spec/barbeque/message_handler/job_retry_spec.rb",
    "chars": 4623,
    "preview": "require 'rails_helper'\nrequire 'barbeque/worker'\n\ndescribe Barbeque::MessageHandler::JobRetry do\n  describe '#run' do\n  "
  },
  {
    "path": "spec/barbeque/message_handler/notification_spec.rb",
    "chars": 898,
    "preview": "require 'rails_helper'\nrequire 'barbeque/worker'\n\ndescribe Barbeque::MessageHandler::Notification do\n  describe '#initia"
  },
  {
    "path": "spec/barbeque/message_queue_spec.rb",
    "chars": 1333,
    "preview": "require 'rails_helper'\nrequire 'barbeque/worker'\n\ndescribe Barbeque::MessageQueue do\n  describe '#dequeue' do\n    let(:j"
  },
  {
    "path": "spec/barbeque/message_spec.rb",
    "chars": 3042,
    "preview": "require 'barbeque/worker'\nrequire 'rails_helper'\n\ndescribe Barbeque::Message::Base do\n  let(:application) { 'cookpad' }\n"
  },
  {
    "path": "spec/barbeque/retry_poller_spec.rb",
    "chars": 758,
    "preview": "require 'rails_helper'\nrequire 'barbeque/retry_poller'\n\nRSpec.describe Barbeque::RetryPoller do\n  let(:job_queue) { crea"
  },
  {
    "path": "spec/barbeque/runner_spec.rb",
    "chars": 2553,
    "preview": "require 'rails_helper'\nrequire 'barbeque/runner'\n\nRSpec.describe Barbeque::Runner do\n  let(:job_queue) { create(:job_que"
  },
  {
    "path": "spec/barbeque/worker_spec.rb",
    "chars": 3649,
    "preview": "require 'rails_helper'\nrequire 'barbeque/worker'\n\ndescribe Barbeque::Worker do\n  let!(:job_execution) { create(:job_exec"
  },
  {
    "path": "spec/controllers/barbeque/apps_controller_spec.rb",
    "chars": 2219,
    "preview": "require 'rails_helper'\n\ndescribe Barbeque::AppsController do\n  routes { Barbeque::Engine.routes }\n\n  describe '#index' d"
  },
  {
    "path": "spec/controllers/barbeque/job_definitions_controller_spec.rb",
    "chars": 8565,
    "preview": "require 'rails_helper'\n\ndescribe Barbeque::JobDefinitionsController do\n  routes { Barbeque::Engine.routes }\n\n  describe "
  },
  {
    "path": "spec/controllers/barbeque/job_executions_controller_spec.rb",
    "chars": 2947,
    "preview": "require 'rails_helper'\nrequire 'barbeque/execution_log'\n\ndescribe Barbeque::JobExecutionsController do\n  routes { Barbeq"
  },
  {
    "path": "spec/controllers/barbeque/job_queues_controller_spec.rb",
    "chars": 3589,
    "preview": "require 'rails_helper'\n\ndescribe Barbeque::JobQueuesController do\n  routes { Barbeque::Engine.routes }\n\n  describe '#ind"
  },
  {
    "path": "spec/controllers/barbeque/job_retries_controller_spec.rb",
    "chars": 1550,
    "preview": "require 'rails_helper'\nrequire 'barbeque/execution_log'\n\ndescribe Barbeque::JobRetriesController do\n  routes { Barbeque:"
  },
  {
    "path": "spec/controllers/barbeque/sns_subscriptions_controller_spec.rb",
    "chars": 5523,
    "preview": "require 'rails_helper'\n\ndescribe Barbeque::SnsSubscriptionsController do\n  routes { Barbeque::Engine.routes }\n\n  before "
  },
  {
    "path": "spec/dummy/.gitignore",
    "chars": 468,
    "preview": "# See https://help.github.com/articles/ignoring-files for more about ignoring files.\n#\n# If you find yourself ignoring t"
  },
  {
    "path": "spec/dummy/Rakefile",
    "chars": 227,
    "preview": "# Add your own tasks in files placed in lib/tasks ending in .rake,\n# for example lib/tasks/capistrano.rake, and they wil"
  },
  {
    "path": "spec/dummy/app/assets/config/manifest.js",
    "chars": 101,
    "preview": "//= link_tree ../images\n//= link_directory ../javascripts .js\n//= link_directory ../stylesheets .css\n"
  },
  {
    "path": "spec/dummy/app/assets/images/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spec/dummy/app/assets/javascripts/application.js",
    "chars": 712,
    "preview": "// This is a manifest file that'll be compiled into application.js, which will include all the files\n// listed below.\n//"
  },
  {
    "path": "spec/dummy/app/assets/javascripts/channels/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spec/dummy/app/assets/stylesheets/application.css",
    "chars": 736,
    "preview": "/*\n * This is a manifest file that'll be compiled into application.css, which will include all the files\n * listed below"
  },
  {
    "path": "spec/dummy/app/controllers/application_controller.rb",
    "chars": 97,
    "preview": "class ApplicationController < ActionController::Base\n  protect_from_forgery with: :exception\nend\n"
  },
  {
    "path": "spec/dummy/app/controllers/concerns/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spec/dummy/app/helpers/application_helper.rb",
    "chars": 29,
    "preview": "module ApplicationHelper\nend\n"
  },
  {
    "path": "spec/dummy/app/models/concerns/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spec/dummy/app/views/layouts/application.html.erb",
    "chars": 249,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Dummy</title>\n    <%= csrf_meta_tags %>\n\n    <%= stylesheet_link_tag    'appl"
  },
  {
    "path": "spec/dummy/app/views/layouts/mailer.html.erb",
    "chars": 229,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <style>\n  "
  },
  {
    "path": "spec/dummy/app/views/layouts/mailer.text.erb",
    "chars": 13,
    "preview": "<%= yield %>\n"
  },
  {
    "path": "spec/dummy/bin/bundle",
    "chars": 125,
    "preview": "#!/usr/bin/env ruby\nENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)\nload Gem.bin_path('bundler', 'bund"
  },
  {
    "path": "spec/dummy/bin/rails",
    "chars": 141,
    "preview": "#!/usr/bin/env ruby\nAPP_PATH = File.expand_path(\"../config/application\", __dir__)\nrequire_relative \"../config/boot\"\nrequ"
  },
  {
    "path": "spec/dummy/bin/rake",
    "chars": 90,
    "preview": "#!/usr/bin/env ruby\nrequire_relative \"../config/boot\"\nrequire \"rake\"\nRake.application.run\n"
  },
  {
    "path": "spec/dummy/bin/setup",
    "chars": 1010,
    "preview": "#!/usr/bin/env ruby\nrequire \"fileutils\"\n\n# path to your application root.\nAPP_ROOT = File.expand_path(\"..\", __dir__)\n\nde"
  },
  {
    "path": "spec/dummy/bin/spring",
    "chars": 488,
    "preview": "#!/usr/bin/env ruby\n\n# This file loads spring without using Bundler, in order to be fast.\n# It gets overwritten when you"
  },
  {
    "path": "spec/dummy/bin/update",
    "chars": 819,
    "preview": "#!/usr/bin/env ruby\nrequire 'fileutils'\ninclude FileUtils\n\n# path to your application root.\nAPP_ROOT = File.expand_path("
  },
  {
    "path": "spec/dummy/bin/yarn",
    "chars": 303,
    "preview": "#!/usr/bin/env ruby\nAPP_ROOT = File.expand_path('..', __dir__)\nDir.chdir(APP_ROOT) do\n  begin\n    exec \"yarnpkg\", *ARGV\n"
  },
  {
    "path": "spec/dummy/config/application.rb",
    "chars": 1173,
    "preview": "require_relative \"boot\"\n\nrequire \"rails\"\n# Pick the frameworks you want:\nrequire \"active_model/railtie\"\n# require \"activ"
  },
  {
    "path": "spec/dummy/config/barbeque.empty.yml",
    "chars": 9,
    "preview": "test: {}\n"
  },
  {
    "path": "spec/dummy/config/barbeque.erb.yml",
    "chars": 39,
    "preview": "test:\n  executor: <%= 'Foo' + 'Bar' %>\n"
  },
  {
    "path": "spec/dummy/config/barbeque.hako.yml",
    "chars": 322,
    "preview": "default: &default\n  executor: Hako\n  executor_options:\n    hako_dir: /home/k0kubun/hako_repo\n    hako_env:\n      ACCESS_"
  },
  {
    "path": "spec/dummy/config/barbeque.yml",
    "chars": 149,
    "preview": "default: &default\n  exception_handler: RailsLogger\n  executor: Docker\n\ndevelopment:\n  <<: *default\n\ntest:\n  <<: *default"
  },
  {
    "path": "spec/dummy/config/boot.rb",
    "chars": 128,
    "preview": "ENV[\"BUNDLE_GEMFILE\"] ||= File.expand_path(\"../Gemfile\", __dir__)\n\nrequire \"bundler/setup\" # Set up gems listed in the G"
  },
  {
    "path": "spec/dummy/config/database.yml",
    "chars": 1590,
    "preview": "# MySQL. Versions 5.0 and up are supported.\n#\n# Install the MySQL driver\n#   gem install mysql2\n#\n# Ensure the MySQL gem"
  },
  {
    "path": "spec/dummy/config/environment.rb",
    "chars": 128,
    "preview": "# Load the Rails application.\nrequire_relative \"application\"\n\n# Initialize the Rails application.\nRails.application.init"
  },
  {
    "path": "spec/dummy/config/environments/development.rb",
    "chars": 2137,
    "preview": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n  # Settings specified here will take pre"
  },
  {
    "path": "spec/dummy/config/environments/production.rb",
    "chars": 3038,
    "preview": "require \"active_support/core_ext/integer/time\"\n\nRails.application.configure do\n  # Settings specified here will take pre"
  },
  {
    "path": "spec/dummy/config/environments/test.rb",
    "chars": 2008,
    "preview": "require \"active_support/core_ext/integer/time\"\n\n# The test environment is used exclusively to run your application's\n# t"
  }
]

// ... and 53 more files (download for full content)

About this extraction

This page contains the full source code of the cookpad/barbeque GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 253 files (2.5 MB), approximately 660.6k tokens, and a symbol index with 1455 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!