barbeque 0.0.1 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -3
  3. data/Rakefile +2 -6
  4. data/app/assets/javascripts/barbeque/application.js +5 -0
  5. data/app/assets/javascripts/barbeque/job_definitions.coffee +10 -0
  6. data/app/assets/stylesheets/barbeque/application.scss +7 -0
  7. data/app/assets/stylesheets/barbeque/common.scss +22 -0
  8. data/app/assets/stylesheets/barbeque/job_definitions.scss +21 -0
  9. data/app/controllers/barbeque/api/application_controller.rb +22 -0
  10. data/app/controllers/barbeque/api/job_executions_controller.rb +35 -0
  11. data/app/controllers/barbeque/api/job_retries_controller.rb +28 -0
  12. data/app/controllers/barbeque/api/revision_locks_controller.rb +34 -0
  13. data/app/controllers/barbeque/apps_controller.rb +43 -0
  14. data/app/controllers/barbeque/job_definitions_controller.rb +86 -0
  15. data/app/controllers/barbeque/job_executions_controller.rb +19 -0
  16. data/app/controllers/barbeque/job_queues_controller.rb +59 -0
  17. data/app/controllers/barbeque/job_retries_controller.rb +10 -0
  18. data/app/helpers/barbeque/job_definitions_helper.rb +14 -0
  19. data/app/helpers/barbeque/job_executions_helper.rb +18 -0
  20. data/app/models/{api → barbeque/api}/application_resource.rb +3 -1
  21. data/app/models/{api → barbeque/api}/job_execution_resource.rb +1 -1
  22. data/app/models/{api → barbeque/api}/job_retry_resource.rb +1 -1
  23. data/app/models/barbeque/api/revision_lock_resource.rb +7 -0
  24. data/app/models/{app.rb → barbeque/app.rb} +1 -1
  25. data/app/models/barbeque/job_definition.rb +26 -0
  26. data/app/models/{job_execution.rb → barbeque/job_execution.rb} +7 -4
  27. data/app/models/{job_queue.rb → barbeque/job_queue.rb} +1 -1
  28. data/app/models/{job_retry.rb → barbeque/job_retry.rb} +7 -2
  29. data/app/models/{slack_notification.rb → barbeque/slack_notification.rb} +1 -1
  30. data/app/services/barbeque/message_enqueuing_service.rb +41 -0
  31. data/app/services/barbeque/message_retrying_service.rb +32 -0
  32. data/app/views/barbeque/apps/_form.html.haml +34 -0
  33. data/app/views/barbeque/apps/edit.html.haml +3 -0
  34. data/app/views/barbeque/apps/index.html.haml +24 -0
  35. data/app/views/barbeque/apps/new.html.haml +3 -0
  36. data/app/views/barbeque/apps/show.html.haml +47 -0
  37. data/app/views/barbeque/job_definitions/_form.html.haml +45 -0
  38. data/app/views/barbeque/job_definitions/_slack_notification_field.html.haml +35 -0
  39. data/app/views/barbeque/job_definitions/edit.html.haml +3 -0
  40. data/app/views/barbeque/job_definitions/index.html.haml +24 -0
  41. data/app/views/barbeque/job_definitions/new.html.haml +3 -0
  42. data/app/views/barbeque/job_definitions/show.html.haml +90 -0
  43. data/app/views/barbeque/job_definitions/stats.html.haml +52 -0
  44. data/app/views/barbeque/job_executions/show.html.haml +92 -0
  45. data/app/views/barbeque/job_queues/_form.html.haml +29 -0
  46. data/app/views/barbeque/job_queues/edit.html.haml +3 -0
  47. data/app/views/barbeque/job_queues/index.html.haml +22 -0
  48. data/app/views/barbeque/job_queues/new.html.haml +3 -0
  49. data/app/views/barbeque/job_queues/show.html.haml +22 -0
  50. data/app/views/barbeque/job_retries/show.html.haml +59 -0
  51. data/app/views/layouts/barbeque/_header.html.haml +10 -0
  52. data/app/views/layouts/barbeque/_sidebar.html.haml +16 -0
  53. data/app/views/layouts/barbeque/application.html.haml +24 -0
  54. data/app/views/layouts/barbeque/apps.html.haml +6 -0
  55. data/app/views/layouts/barbeque/job_definitions.html.haml +6 -0
  56. data/app/views/layouts/barbeque/job_executions.html.haml +5 -0
  57. data/app/views/layouts/barbeque/job_queues.html.haml +6 -0
  58. data/config/initializers/garage.rb +4 -0
  59. data/config/routes.rb +32 -0
  60. data/db/migrate/20160829023237_prefix_barbeque_to_tables.rb +10 -0
  61. data/lib/barbeque.rb +3 -5
  62. data/lib/barbeque/configuration.rb +19 -0
  63. data/lib/barbeque/docker_image.rb +20 -0
  64. data/lib/barbeque/engine.rb +11 -0
  65. data/lib/barbeque/execution_log.rb +51 -0
  66. data/lib/barbeque/message.rb +28 -0
  67. data/lib/barbeque/message/base.rb +29 -0
  68. data/lib/barbeque/message/invalid_message.rb +11 -0
  69. data/lib/barbeque/message/job_execution.rb +20 -0
  70. data/lib/barbeque/message/job_retry.rb +16 -0
  71. data/lib/barbeque/message_handler.rb +8 -0
  72. data/lib/barbeque/message_handler/job_execution.rb +86 -0
  73. data/lib/barbeque/message_handler/job_retry.rb +83 -0
  74. data/lib/barbeque/message_queue.rb +66 -0
  75. data/lib/barbeque/runner.rb +12 -0
  76. data/lib/barbeque/runner/docker.rb +34 -0
  77. data/lib/barbeque/slack_client.rb +48 -0
  78. data/lib/barbeque/version.rb +1 -1
  79. data/lib/barbeque/worker.rb +58 -0
  80. data/lib/tasks/barbeque_tasks.rake +9 -4
  81. metadata +272 -18
  82. data/app/assets/stylesheets/barbeque/application.css +0 -15
  83. data/app/models/application_record.rb +0 -3
  84. data/app/models/job_definition.rb +0 -14
  85. data/app/views/layouts/barbeque/application.html.erb +0 -14
@@ -0,0 +1,24 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %meta{ content: 'text/html; charset=UTF-8', 'http-equiv': 'Content-Type' }
5
+ %meta{ charset: 'UTF-8' }
6
+ %title Barbeque
7
+ %meta{ content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no', name: 'viewport' }
8
+ -# FIXME: Don't rely on others' CDN
9
+ %link{ href: 'https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css', rel: 'stylesheet', type: 'text/css' }
10
+ %link{ href: 'https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css', rel: 'stylesheet', type: 'text/css' }
11
+ = stylesheet_link_tag 'barbeque/application', media: 'all'
12
+ = javascript_include_tag 'barbeque/application'
13
+ = csrf_meta_tags
14
+
15
+ %body.skin-blue.sidebar-mini{ class: "#{controller_path.parameterize(separator: '_')}_controller #{action_name}_action" }
16
+ .wrapper
17
+ = render partial: 'layouts/barbeque/header'
18
+ = render partial: 'layouts/barbeque/sidebar'
19
+ .content-wrapper
20
+ - if content_for?(:header)
21
+ %section.content-header
22
+ = content_for(:header)
23
+ %section.content
24
+ = yield
@@ -0,0 +1,6 @@
1
+ - content_for(:header) do
2
+ %h1
3
+ .fa.fa-users
4
+ Applications
5
+
6
+ = render template: 'layouts/barbeque/application'
@@ -0,0 +1,6 @@
1
+ - content_for(:header) do
2
+ %h1
3
+ .fa.fa-tasks
4
+ Job Definitions
5
+
6
+ = render template: 'layouts/barbeque/application'
@@ -0,0 +1,5 @@
1
+ - content_for(:header) do
2
+ %h1
3
+ Job Executions
4
+
5
+ = render template: 'layouts/barbeque/application'
@@ -0,0 +1,6 @@
1
+ - content_for(:header) do
2
+ %h1
3
+ .fa.fa-cubes
4
+ Queues
5
+
6
+ = render template: 'layouts/barbeque/application'
@@ -0,0 +1,4 @@
1
+ require 'garage'
2
+
3
+ Garage.configure {}
4
+ Garage.configuration.strategy = Garage::Strategy::NoAuthentication
@@ -1,2 +1,34 @@
1
1
  Barbeque::Engine.routes.draw do
2
+ root to: 'apps#index'
3
+
4
+ resources :apps, except: :index
5
+
6
+ resources :job_definitions do
7
+ get :stats
8
+ get :execution_stats
9
+ end
10
+
11
+ resources :job_executions, only: :show do
12
+ post :retry
13
+
14
+ resources :job_retries, only: :show
15
+ end
16
+
17
+ resources :job_queues
18
+
19
+ scope :v1, module: 'api', as: :v1 do
20
+ resources :apps, only: [], param: :name, constraints: { name: /[\w-]+/ } do
21
+ resource :revision_lock, only: [:create, :destroy]
22
+ end
23
+
24
+ resources :job_executions, only: :show, param: :message_id,
25
+ constraints: { message_id: /[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}/ } do
26
+ resources :job_retries, only: [:create], path: 'retries'
27
+ end
28
+ end
29
+
30
+ scope :v2, module: 'api', as: :v2 do
31
+ resources :job_executions, only: :create, param: :message_id,
32
+ constraints: { message_id: /[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}/ }
33
+ end
2
34
  end
@@ -0,0 +1,10 @@
1
+ class PrefixBarbequeToTables < ActiveRecord::Migration[5.0]
2
+ def change
3
+ rename_table :apps, :barbeque_apps
4
+ rename_table :job_definitions, :barbeque_job_definitions
5
+ rename_table :job_executions, :barbeque_job_executions
6
+ rename_table :job_queues, :barbeque_job_queues
7
+ rename_table :job_retries, :barbeque_job_retries
8
+ rename_table :slack_notifications, :barbeque_slack_notifications
9
+ end
10
+ end
@@ -1,5 +1,3 @@
1
- require "barbeque/engine"
2
-
3
- module Barbeque
4
- # Your code goes here...
5
- end
1
+ require 'barbeque/docker_image'
2
+ require 'barbeque/engine'
3
+ require 'barbeque/execution_log'
@@ -0,0 +1,19 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'hashie'
4
+
5
+ module Barbeque
6
+ module Configuration
7
+ def config
8
+ @config ||= build_config
9
+ end
10
+
11
+ def build_config
12
+ filename = Rails.root.join('config', 'barbeque.yml').to_s
13
+ hash = YAML.load(ERB.new(File.read(filename)).result)
14
+ Hashie::Mash.new(hash[Rails.env])
15
+ end
16
+ end
17
+
18
+ extend Configuration
19
+ end
@@ -0,0 +1,20 @@
1
+ module Barbeque
2
+ class DockerImage
3
+ DEFAULT_TAG = 'latest'
4
+
5
+ def initialize(str)
6
+ # See: https://github.com/docker/docker/blob/v1.10.2/image/spec/v1.md
7
+ result = str.match(%r{((?<registry>[^/]+)?/)?(?<repository>[\w./-]+)(:(?<tag>[\w.-]+))?\z})
8
+ @repository = result[:repository]
9
+ @tag = result[:tag] || DEFAULT_TAG
10
+ @registry = result[:registry] || ENV['BARBEQUE_DOCKER_REGISTRY']
11
+ end
12
+
13
+ attr_reader :registry, :repository
14
+ attr_accessor :tag
15
+
16
+ def to_s
17
+ [registry, "#{repository}:#{tag}"].compact.join('/')
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,16 @@
1
1
  module Barbeque
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Barbeque
4
+
5
+ config.before_configuration do
6
+ # Listing gems which are mountable engine or have railtie.
7
+ require 'adminlte2-rails'
8
+ require 'coffee-rails'
9
+ require 'hamlit'
10
+ require 'jquery-rails'
11
+ require 'kaminari'
12
+ require 'sass-rails'
13
+ require 'weak_parameters'
14
+ end
4
15
  end
5
16
  end
@@ -0,0 +1,51 @@
1
+ require 'aws-sdk'
2
+ require 'active_support'
3
+ require 'active_support/core_ext'
4
+
5
+ module Barbeque
6
+ class ExecutionLog
7
+ DEFAULT_S3_BUCKET_NAME = 'barbeque'
8
+
9
+ class << self
10
+ delegate :save, :load, to: :new
11
+ end
12
+
13
+ # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
14
+ # @param [Hash] log
15
+ def save(execution:, log:)
16
+ s3.put_object(
17
+ bucket: s3_bucket_name,
18
+ key: s3_key_for(execution: execution),
19
+ body: log.to_json,
20
+ )
21
+ end
22
+
23
+ # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
24
+ # @return [Hash] log
25
+ def load(execution:)
26
+ return {} if execution.pending?
27
+
28
+ s3_object = s3.get_object(
29
+ bucket: s3_bucket_name,
30
+ key: s3_key_for(execution: execution),
31
+ )
32
+ JSON.load(s3_object.body.read)
33
+ end
34
+
35
+ private
36
+
37
+ def s3_bucket_name
38
+ @s3_bucket_name ||= ENV['BARBEQUE_S3_BUCKET_NAME'] || DEFAULT_S3_BUCKET_NAME
39
+ end
40
+
41
+ # @param [Barbeque::JobExecution,Barbeque::JobRetry] execution
42
+ # @param [String] message_id
43
+ def s3_key_for(execution:)
44
+ File.join(execution.app.name, execution.job_definition.job, execution.message_id)
45
+ end
46
+
47
+ def s3
48
+ @s3 ||= Aws::S3::Client.new
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ require 'barbeque/message/base'
2
+ require 'barbeque/message/invalid_message'
3
+ require 'barbeque/message/job_execution'
4
+ require 'barbeque/message/job_retry'
5
+
6
+ module Barbeque
7
+ module Message
8
+ class << self
9
+ # @param [Aws::SQS::Types::Message] raw_message
10
+ # @return [Barbeque::Message::Base]
11
+ def parse(raw_message)
12
+ body = JSON.parse(raw_message.body)
13
+ klass = find_class(body['Type'])
14
+ klass.new(raw_message, body)
15
+ rescue JSON::ParserError
16
+ InvalidMessage.new(raw_message, {})
17
+ end
18
+
19
+ private
20
+
21
+ def find_class(type)
22
+ Message.const_get(type, false)
23
+ rescue NameError
24
+ InvalidMessage
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module Barbeque
2
+ module Message
3
+ # A model wrapping Aws::SQS::Types::Message.
4
+ class Base
5
+ attr_reader :id # [String] Barbeque::JobExecution is associated via `message_id`
6
+ attr_reader :receipt_handle # [String] Used to ack a message
7
+ attr_reader :type # [String] "JobExecution", "JobRetry", etc
8
+
9
+ # @param [Aws::SQS::Types::Message] raw_message
10
+ # @param [Hash] parse result of `raw_message.body`
11
+ def initialize(raw_message, message_body)
12
+ assign_body(message_body)
13
+ @id = raw_message.message_id
14
+ @receipt_handle = raw_message.receipt_handle
15
+ end
16
+
17
+ # To distinguish with `Barbeque::Message::InvalidMessage`
18
+ def valid?
19
+ true
20
+ end
21
+
22
+ private
23
+
24
+ def assign_body(message_body)
25
+ @type = message_body['Type']
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ require 'barbeque/message/base'
2
+
3
+ module Barbeque
4
+ module Message
5
+ class InvalidMessage < Base
6
+ def valid?
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ require 'barbeque/message/base'
2
+
3
+ module Barbeque
4
+ module Message
5
+ class JobExecution < Base
6
+ attr_reader :body # [Object] free-format JSON
7
+ attr_reader :application # [String] To specify `job_definitions.application_id`
8
+ attr_reader :job # [String] To specify `job_definitions.name`
9
+
10
+ private
11
+
12
+ def assign_body(message_body)
13
+ super
14
+ @application = message_body['Application']
15
+ @job = message_body['Job']
16
+ @body = message_body['Message']
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ require 'barbeque/message/base'
2
+
3
+ module Barbeque
4
+ module Message
5
+ class JobRetry < Base
6
+ attr_reader :retry_message_id # [String] JobExection's message_id
7
+
8
+ private
9
+
10
+ def assign_body(message_body)
11
+ super
12
+ @retry_message_id = message_body['RetryMessageId']
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ module Barbeque
2
+ module MessageHandler
3
+ class DuplicatedExecution < StandardError; end
4
+ end
5
+ end
6
+
7
+ require 'barbeque/message_handler/job_execution'
8
+ require 'barbeque/message_handler/job_retry'
@@ -0,0 +1,86 @@
1
+ require 'barbeque/docker_image'
2
+ require 'barbeque/execution_log'
3
+ require 'barbeque/runner'
4
+ require 'barbeque/slack_client'
5
+
6
+ module Barbeque
7
+ module MessageHandler
8
+ class JobExecution
9
+ # @param [Barbeque::Message::JobExecution] message
10
+ # @param [Barbeque::JobQueue] job_queue
11
+ def initialize(message:, job_queue:)
12
+ @message = message
13
+ @job_queue = job_queue
14
+ end
15
+
16
+ def run
17
+ job_execution = Barbeque::JobExecution.find_or_initialize_by(message_id: @message.id)
18
+ raise DuplicatedExecution if job_execution.persisted?
19
+ job_execution.update!(job_definition: job_definition, job_queue_id: @job_queue.id)
20
+
21
+ stdout, stderr, status = run_command
22
+ job_execution.update!(status: status.success? ? :success : :failed, finished_at: Time.now)
23
+ notify_slack(job_execution, status)
24
+
25
+ log_result(job_execution, stdout, stderr)
26
+ end
27
+
28
+ private
29
+
30
+ def log_result(execution, stdout, stderr)
31
+ log = { message: @message.body.to_json, stdout: stdout, stderr: stderr }
32
+ Barbeque::ExecutionLog.save(execution: execution, log: log)
33
+ end
34
+
35
+ def notify_slack(job_execution, status)
36
+ return if job_execution.slack_notification.nil?
37
+
38
+ client = Barbeque::SlackClient.new(job_execution.slack_notification.channel)
39
+ if status.success?
40
+ if job_execution.slack_notification.notify_success
41
+ client.notify_success("*[SUCCESS]* Succeeded to execute #{job_execution_link(job_execution)}")
42
+ end
43
+ else
44
+ client.notify_failure(
45
+ "*[FAILURE]* Failed to execute #{job_execution_link(job_execution)}" \
46
+ " #{job_execution.slack_notification.failure_notification_text}"
47
+ )
48
+ end
49
+ end
50
+
51
+ def job_execution_link(job_execution)
52
+ "<#{job_execution_url(job_execution)}|#{job_execution.job_definition.job} ##{job_execution.id}>"
53
+ end
54
+
55
+ def job_execution_url(job_execution)
56
+ Barbeque::Engine.routes.url_helpers.job_execution_url(job_execution, host: ENV['BARBEQUE_HOST'])
57
+ end
58
+
59
+ # @return [String] stdout
60
+ # @return [String] stderr
61
+ # @return [Process::Status] status
62
+ def run_command
63
+ image = DockerImage.new(job_definition.app.docker_image)
64
+ runner = Runner.create(docker_image: image)
65
+ runner.run(job_definition.command, job_envs)
66
+ end
67
+
68
+ def job_envs
69
+ {
70
+ 'BARBEQUE_JOB' => @message.job,
71
+ 'BARBEQUE_MESSAGE' => @message.body.to_json,
72
+ 'BARBEQUE_MESSAGE_ID' => @message.id,
73
+ 'BARBEQUE_QUEUE_NAME' => @job_queue.name,
74
+ 'BARBEQUE_RETRY_COUNT' => '0',
75
+ }
76
+ end
77
+
78
+ def job_definition
79
+ @job_definition ||= Barbeque::JobDefinition.joins(:app).find_by!(
80
+ job: @message.job,
81
+ barbeque_apps: { name: @message.application },
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,83 @@
1
+ require 'barbeque/docker_image'
2
+ require 'barbeque/execution_log'
3
+ require 'barbeque/runner'
4
+ require 'barbeque/slack_client'
5
+
6
+ module Barbeque
7
+ module MessageHandler
8
+ class JobRetry
9
+ # @param [Barbeque::Message::JobExecution] message
10
+ # @param [Barbeque::JobQueue] job_queue
11
+ def initialize(message:, job_queue:)
12
+ @message = message
13
+ @job_queue = job_queue
14
+ end
15
+
16
+ def run
17
+ job_retry = Barbeque::JobRetry.find_or_initialize_by(message_id: @message.id)
18
+ job_retry.update!(job_execution: job_execution)
19
+ job_execution.update!(status: 'retried')
20
+
21
+ stdout, stderr, result = run_command
22
+ status = result.success? ? :success : :failed
23
+ job_retry.update!(status: status, finished_at: Time.now)
24
+ job_execution.update!(status: status)
25
+ notify_slack(job_retry, result)
26
+
27
+ log_result(job_retry, stdout, stderr)
28
+ end
29
+
30
+ private
31
+
32
+ def log_result(job_retry, stdout, stderr)
33
+ log = { stdout: stdout, stderr: stderr }
34
+ Barbeque::ExecutionLog.save(execution: job_retry, log: log)
35
+ end
36
+
37
+ # @param [Barbeque::JobRetry] job_retry
38
+ # @param [Process::Status] result
39
+ def notify_slack(job_retry, result)
40
+ return if job_retry.slack_notification.nil?
41
+
42
+ client = Barbeque::SlackClient.new(job_retry.slack_notification.channel)
43
+ if result.success?
44
+ if job_retry.slack_notification.notify_success
45
+ client.notify_success("*[SUCCESS]* Succeeded to retry #{job_retry_link(job_retry)}")
46
+ end
47
+ else
48
+ client.notify_failure(
49
+ "*[FAILURE]* Failed to retry #{job_retry_link(job_retry)}" \
50
+ " #{job_execution.slack_notification.failure_notification_text}"
51
+ )
52
+ end
53
+ end
54
+
55
+ def job_retry_link(job_retry)
56
+ url = Barbeque::Engine.routes.url_helpers.job_execution_job_retry_url(
57
+ job_retry.job_execution, job_retry, host: ENV['BARBEQUE_HOST']
58
+ )
59
+ "<#{url}|#{job_retry.job_definition.job}'s retry ##{job_retry.id}>"
60
+ end
61
+
62
+ def job_execution
63
+ @job_execution ||= Barbeque::JobExecution.find_by!(message_id: @message.retry_message_id)
64
+ end
65
+
66
+ def run_command
67
+ image = DockerImage.new(job_execution.app.docker_image)
68
+ runner = Runner.create(docker_image: image)
69
+ runner.run(job_execution.job_definition.command, job_envs)
70
+ end
71
+
72
+ def job_envs
73
+ {
74
+ 'BARBEQUE_JOB' => job_execution.job_definition.job,
75
+ 'BARBEQUE_MESSAGE' => job_execution.execution_log['message'],
76
+ 'BARBEQUE_MESSAGE_ID' => @message.retry_message_id,
77
+ 'BARBEQUE_QUEUE_NAME' => @job_queue.name,
78
+ 'BARBEQUE_RETRY_COUNT' => job_execution.job_retries.count.to_s,
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end