wrapbox 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: edc9210efdc2d7248e0b6eab05f04c89f7223b90
4
- data.tar.gz: afe62fcbfbf5f5383b12e476709fd7d79a36af27
3
+ metadata.gz: 8118b81067d91ea3535d8c3c5ca1ba55c412d345
4
+ data.tar.gz: 33fb0eb243ff53a6bc6ec7c5147a69ea3bbbc206
5
5
  SHA512:
6
- metadata.gz: 8f856b7dc58a1776c662f4bd43d1165c043c4ec8a2e5ad0a12eac1b4c60a1162d9ef57541c1d54373471f59879f6257cd8ae1105c1b26e142ffc790839694f75
7
- data.tar.gz: 0cc10bf941d6b73d320581ae722383c888f5cac1fe700bfccfd5b5ab117abd3b1347abb5f67229d8048d1ff784768224122dd8c767f7188888489b1877a0e559
6
+ metadata.gz: 5e9a9730bca6b1dddbe9a8c21a2660645dc0d3f6f3c71a74c24e175930a6bd01544e2f2b2efd6d04fc0b14bdbeeded4a4d5ad7abe38e37c4c5abe83593c1a37c
7
+ data.tar.gz: 74c5c6831768ed37e5107903426131a4158220b808b39fae6d08b74df14dcfcc2f9bd495ec1ce3f6157a66ee5602f08a5161e0c73b6535f1cefdf4ebbf14a388
data/README.md CHANGED
@@ -42,6 +42,17 @@ docker:
42
42
  memory: 1024
43
43
  ```
44
44
 
45
+ #### run by CLI
46
+
47
+ ```sh
48
+ $ wrapbox ecs run_cmd -f config.yml \
49
+ -e "FOO=bar,HOGE=fuga" \
50
+ "bundle exec rspec spec/models" \
51
+ "bundle exec rspec spec/controllers" \
52
+ ```
53
+
54
+ #### run by ruby
55
+
45
56
  ```ruby
46
57
  Wrapbox.configure do |c|
47
58
  c.load_yaml(File.expand_path("../config.yml", __FILE__))
@@ -53,10 +64,7 @@ Wrapbox.run("TestJob", :perform, ["arg1", ["arg2", "arg3"]], environments: [{nam
53
64
  Wrapbox.run("TestJob", :perform, ["arg1", ["arg2", "arg3"]], config_name: :docker, environments: [{name: "RAILS_ENV", value: "development"}]) # use docker config
54
65
 
55
66
  # runs ls . command in ECS container
56
- Wrapbox.run_cmd("ls", ".", environments: [{name: "RAILS_ENV", value: "development"}])
57
-
58
- # runs ls . command in local docker container
59
- Wrapbox.run_cmd("ls", ".", config_name: :docker, environments: [{name: "RAILS_ENV", value: "development"}])
67
+ Wrapbox.run_cmd(["ls ."], environments: [{name: "RAILS_ENV", value: "development"}])
60
68
  ```
61
69
 
62
70
  ## Config
@@ -86,9 +94,9 @@ Wrapbox.run_cmd("ls", ".", config_name: :docker, environments: [{name: "RAILS_EN
86
94
 
87
95
  ## API
88
96
 
89
- #### `run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10)`
97
+ #### `run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)`
90
98
 
91
- #### `run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10)`
99
+ #### `run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)`
92
100
 
93
101
  following options is only for ECS.
94
102
 
@@ -98,7 +106,7 @@ following options is only for ECS.
98
106
  - launch_timeout
99
107
  - launch_retry
100
108
 
101
- If Wrapbox cannot launch task in `launch_timeout` seconds, it puts custom metric data to CloudWatch.
109
+ If Wrapbox cannot create task, it puts custom metric data to CloudWatch.
102
110
  Custom metric data is `wrapbox/WaitingTaskCount` that has `ClusterName` dimension.
103
111
  And, it retry launching until retry count reach `launch_retry`.
104
112
 
@@ -29,13 +29,18 @@ module Wrapbox
29
29
  exec_docker(definition: definition, cmd: ["bundle", "exec", "rake", "wrapbox:run"], environments: envs)
30
30
  end
31
31
 
32
- def run_cmd(*cmd, container_definition_overrides: {}, environments: [])
32
+ def run_cmd(cmds, container_definition_overrides: {}, environments: [])
33
33
  definition = container_definition
34
34
  .merge(container_definition_overrides)
35
35
 
36
36
  environments = extract_environments(environments)
37
37
 
38
- exec_docker(definition: definition, cmd: cmd, environments: environments)
38
+ ths = cmds.map do |cmd|
39
+ Thread.new(cmd) do |c|
40
+ exec_docker(definition: definition, cmd: c.split(/\s+/), environments: environments)
41
+ end
42
+ end
43
+ ths.each(&:join)
39
44
  end
40
45
 
41
46
  private
@@ -67,7 +72,7 @@ module Wrapbox
67
72
 
68
73
  container = ::Docker::Container.create(options)
69
74
 
70
- container.start!
75
+ container.start
71
76
  output_container_logs(container)
72
77
  resp = container.wait
73
78
  output_container_logs(container)
@@ -97,10 +102,6 @@ module Wrapbox
97
102
  method_option :config_name, aliases: "-n", required: true, default: "default"
98
103
  method_option :environments, aliases: "-e"
99
104
  def run_cmd(*args)
100
- if args.size == 1
101
- args = args[0].split(" ")
102
- end
103
-
104
105
  repo = Wrapbox::ConfigRepository.new.tap { |r| r.load_yaml(options[:config]) }
105
106
  config = repo.get(options[:config_name])
106
107
  config.runner = :docker
@@ -108,7 +109,7 @@ module Wrapbox
108
109
  environments = options[:environments].to_s.split(/,\s*/).map { |kv| kv.split("=") }.map do |k, v|
109
110
  {name: k, value: v}
110
111
  end
111
- runner.run_cmd(*args, environments: environments)
112
+ runner.run_cmd(args, environments: environments)
112
113
  end
113
114
  end
114
115
  end
@@ -11,8 +11,15 @@ require "wrapbox/version"
11
11
  module Wrapbox
12
12
  module Runner
13
13
  class Ecs
14
- class ExecutionError < StandardError; end
14
+ class ExecutionFailure < StandardError; end
15
+ class ContainerAbnormalEnd < StandardError; end
16
+ class ExecutionTimeout < StandardError; end
15
17
  class LaunchFailure < StandardError; end
18
+ class LackResource < StandardError; end
19
+
20
+ EXECUTION_RETRY_INTERVAL = 3
21
+ WAIT_DELAY = 5
22
+ HOST_TERMINATED_REASON_REGEXP = /Host EC2.*terminated/
16
23
 
17
24
  attr_reader \
18
25
  :name,
@@ -31,125 +38,206 @@ module Wrapbox
31
38
  @container_definition = options[:container_definition]
32
39
  @additional_container_definitions = options[:additional_container_definitions]
33
40
  @task_role_arn = options[:task_role_arn]
41
+ $stdout.sync = true
34
42
  @logger = Logger.new($stdout)
35
43
  end
36
44
 
37
- def run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
45
+ class Parameter
46
+ attr_reader \
47
+ :environments,
48
+ :task_role_arn,
49
+ :cluster,
50
+ :timeout,
51
+ :launch_timeout,
52
+ :launch_retry,
53
+ :retry_interval,
54
+ :retry_interval_multiplier,
55
+ :max_retry_interval,
56
+ :execution_retry
57
+
58
+ def initialize(environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)
59
+ b = binding
60
+ method(:initialize).parameters.each do |param|
61
+ instance_variable_set("@#{param[1]}", b.local_variable_get(param[1]))
62
+ end
63
+ end
64
+ end
65
+
66
+ def run(class_name, method_name, args, container_definition_overrides: {}, **parameters)
38
67
  task_definition = register_task_definition(container_definition_overrides)
68
+ parameter = Parameter.new(**parameters)
69
+
39
70
  run_task(
40
71
  task_definition.task_definition_arn, class_name, method_name, args,
41
- command: ["bundle", "exec", "rake", "wrapbox:run"],
42
- environments: environments,
43
- task_role_arn: task_role_arn || @task_role_arn,
44
- cluster: cluster,
45
- timeout: timeout,
46
- launch_timeout: launch_timeout,
47
- launch_retry: launch_retry,
48
- retry_interval: retry_interval,
49
- retry_interval_multiplier: retry_interval_multiplier,
50
- max_retry_interval: max_retry_interval,
72
+ ["bundle", "exec", "rake", "wrapbox:run"],
73
+ parameter
51
74
  )
52
75
  end
53
76
 
54
- def run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
77
+ def run_cmd(cmds, container_definition_overrides: {}, **parameters)
55
78
  task_definition = register_task_definition(container_definition_overrides)
79
+ parameter = Parameter.new(**parameters)
56
80
 
57
- run_task(
58
- task_definition.task_definition_arn, nil, nil, nil,
59
- command: cmd,
60
- environments: environments,
61
- task_role_arn: task_role_arn,
62
- cluster: cluster,
63
- timeout: timeout,
64
- launch_timeout: launch_timeout,
65
- launch_retry: launch_retry,
66
- retry_interval: retry_interval,
67
- retry_interval_multiplier: retry_interval_multiplier,
68
- max_retry_interval: max_retry_interval,
69
- )
81
+ ths = cmds.map do |cmd|
82
+ Thread.new(cmd) do |c|
83
+ run_task(
84
+ task_definition.task_definition_arn, nil, nil, nil,
85
+ c.split(/\s+/),
86
+ parameter
87
+ )
88
+ end
89
+ end
90
+ ths.each(&:join)
91
+ end
92
+
93
+ private
94
+
95
+ def run_task(task_definition_arn, class_name, method_name, args, command, parameter)
96
+ cl = parameter.cluster || self.cluster
97
+ execution_try_count = 0
98
+
99
+ begin
100
+ task = create_task(task_definition_arn, class_name, method_name, args, command, parameter)
101
+
102
+ @logger.debug("Launch Task: #{task.task_arn}")
103
+
104
+ wait_task_stopped(cl, task.task_arn, parameter.timeout)
105
+
106
+ @logger.debug("Stop Task: #{task.task_arn}")
107
+
108
+ # Avoid container exit code fetch miss
109
+ sleep WAIT_DELAY
110
+
111
+ task_status = fetch_task_status(cl, task.task_arn)
112
+
113
+ # If exit_code is nil, Container is force killed or ECS failed to launch Container by Irregular situation
114
+ error_message = "Container #{task_definition_name} is failed. task=#{task.task_arn}, exit_code=#{task_status[:exit_code]}, reason=#{task_status[:stopped_reason]}"
115
+ raise ContainerAbnormalEnd, error_message unless task_status[:exit_code]
116
+ raise ExecutionFailure, error_message unless task_status[:exit_code] == 0
117
+
118
+ true
119
+ rescue ContainerAbnormalEnd
120
+ retry if task_status[:stopped_reason] =~ HOST_TERMINATED_REASON_REGEXP
121
+
122
+ if execution_try_count >= parameter.execution_retry
123
+ raise
124
+ else
125
+ execution_try_count += 1
126
+ @logger.debug("Retry Execution after #{EXECUTION_RETRY_INTERVAL} sec")
127
+ sleep EXECUTION_RETRY_INTERVAL
128
+ retry
129
+ end
130
+ end
131
+ end
132
+
133
+ def task_definition_name
134
+ "wrapbox_#{name}"
70
135
  end
71
136
 
72
- def run_task(task_definition_arn, class_name, method_name, args, command:, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
73
- cl = cluster || self.cluster
137
+ def create_task(task_definition_arn, class_name, method_name, args, command, parameter)
138
+ cl = parameter.cluster || self.cluster
74
139
  args = Array(args)
75
140
 
76
141
  launch_try_count = 0
77
- current_retry_interval = retry_interval
78
- task = nil
79
- exit_code = nil
142
+ current_retry_interval = parameter.retry_interval
143
+
80
144
  begin
81
145
  task = client
82
- .run_task(build_run_task_options(class_name, method_name, args, command, environments, cluster, task_definition_arn, task_role_arn))
146
+ .run_task(build_run_task_options(task_definition_arn, class_name, method_name, args, command, cl, parameter.environments, parameter.task_role_arn))
83
147
  .tasks[0]
84
- raise LaunchFailure unless task
148
+
149
+ raise LackResource unless task # this case is almost lack of container resource.
150
+
85
151
  @logger.debug("Create Task: #{task.task_arn}")
86
- client.wait_until(:tasks_running, cluster: cl, tasks: [task.task_arn]) do |w|
87
- if launch_timeout
88
- w.delay = 5
89
- w.max_attempts = launch_timeout / w.delay
90
- else
91
- w.max_attempts = nil
92
- end
93
- end
94
- rescue Aws::Waiters::Errors::WaiterFailed, LaunchFailure
95
- exit_code = task && fetch_exit_code(cl, task.task_arn)
96
- unless exit_code
97
- if launch_try_count >= launch_retry
98
- client.stop_task(
99
- cluster: cl,
100
- task: task.task_arn,
101
- reason: "launch timeout"
102
- ) if task
103
- raise
104
- else
105
- put_waiting_task_count_metric(cl)
106
- launch_try_count += 1
107
- @logger.debug("Retry Create Task after #{current_retry_interval} sec")
108
- sleep current_retry_interval
109
- current_retry_interval = [current_retry_interval * retry_interval_multiplier, max_retry_interval].min
110
- retry
111
- end
112
- end
113
- end
114
152
 
115
- @logger.debug("Launch Task: #{task.task_arn}")
153
+ # Wait ECS Task Status becomes stable
154
+ sleep WAIT_DELAY
116
155
 
117
- begin
118
- client.wait_until(:tasks_stopped, cluster: cl, tasks: [task.task_arn]) do |w|
119
- if timeout
120
- w.delay = 5
121
- w.max_attempts = timeout / w.delay
156
+ begin
157
+ wait_task_running(cl, task.task_arn, parameter.launch_timeout)
158
+ task
159
+ rescue Aws::Waiters::Errors::TooManyAttemptsError
160
+ client.stop_task(
161
+ cluster: cl,
162
+ task: task.task_arn,
163
+ reason: "launch timeout"
164
+ )
165
+ raise
166
+ rescue Aws::Waiters::Errors::WaiterFailed
167
+ task_status = fetch_task_status(cl, task.task_arn)
168
+
169
+ case task_status[:last_status]
170
+ when "RUNNING"
171
+ return task
172
+ when "PENDING"
173
+ retry
122
174
  else
123
- w.max_attempts = nil
175
+ if task_status[:exit_code]
176
+ return task
177
+ else
178
+ raise LaunchFailure
179
+ end
124
180
  end
125
181
  end
126
- rescue Aws::Waiters::Errors::TooManyAttemptsError
127
- client.stop_task({
128
- cluster: cluster || self.cluster,
129
- task: task.task_arn,
130
- reason: "process timeout",
131
- })
132
- raise ExecutionError, "process timeout"
133
- end
182
+ rescue LackResource
183
+ @logger.debug("Failed to create task, because of lack resource")
184
+ put_waiting_task_count_metric(cl)
134
185
 
135
- @logger.debug("Stop Task: #{task.task_arn}")
136
-
137
- exit_code ||= fetch_exit_code(cl, task.task_arn)
138
- unless exit_code == 0
139
- raise ExecutionError, "Container #{task_definition_name} is failed. exit_code=#{exit_code}"
186
+ if launch_try_count >= parameter.launch_retry
187
+ raise
188
+ else
189
+ launch_try_count += 1
190
+ @logger.debug("Retry Create Task after #{current_retry_interval} sec")
191
+ sleep current_retry_interval
192
+ current_retry_interval = [current_retry_interval * parameter.retry_interval_multiplier, parameter.max_retry_interval].min
193
+ retry
194
+ end
195
+ rescue LaunchFailure
196
+ if launch_try_count >= parameter.launch_retry
197
+ raise
198
+ else
199
+ launch_try_count += 1
200
+ @logger.debug("Retry Create Task after #{current_retry_interval} sec")
201
+ sleep current_retry_interval
202
+ current_retry_interval = [current_retry_interval * parameter.retry_interval_multiplier, parameter.max_retry_interval].min
203
+ retry
204
+ end
140
205
  end
141
206
  end
142
207
 
143
- private
208
+ def wait_task_running(cluster, task_arn, launch_timeout)
209
+ client.wait_until(:tasks_running, cluster: cluster, tasks: [task_arn]) do |w|
210
+ if launch_timeout
211
+ w.delay = WAIT_DELAY
212
+ w.max_attempts = launch_timeout / w.delay
213
+ else
214
+ w.max_attempts = nil
215
+ end
216
+ end
217
+ end
144
218
 
145
- def task_definition_name
146
- "wrapbox_#{name}"
219
+ def wait_task_stopped(cluster, task_arn, timeout)
220
+ client.wait_until(:tasks_stopped, cluster: cluster, tasks: [task_arn]) do |w|
221
+ if timeout
222
+ w.delay = WAIT_DELAY
223
+ w.max_attempts = timeout / w.delay
224
+ else
225
+ w.max_attempts = nil
226
+ end
227
+ end
228
+ rescue Aws::Waiters::Errors::TooManyAttemptsError
229
+ client.stop_task({
230
+ cluster: cl,
231
+ task: task_arn,
232
+ reason: "process timeout",
233
+ })
234
+ raise ExecutionTimeout, "Container #{task_definition_name} is timeout. task=#{task_arn}, timeout=#{timeout}"
147
235
  end
148
236
 
149
- def fetch_exit_code(cluster, task_arn)
237
+ def fetch_task_status(cluster, task_arn)
150
238
  task = client.describe_tasks(cluster: cluster, tasks: [task_arn]).tasks[0]
151
239
  container = task.containers.find { |c| c.name = task_definition_name }
152
- container.exit_code
240
+ {last_status: task.last_status, exit_code: container.exit_code, stopped_reason: task.stopped_reason}
153
241
  end
154
242
 
155
243
  def register_task_definition(container_definition_overrides)
@@ -165,10 +253,18 @@ module Wrapbox
165
253
  end
166
254
  end
167
255
 
168
- client.register_task_definition({
169
- family: task_definition_name,
170
- container_definitions: container_definitions,
171
- }).task_definition
256
+ register_retry_count = 0
257
+ begin
258
+ client.register_task_definition({
259
+ family: task_definition_name,
260
+ container_definitions: container_definitions,
261
+ }).task_definition
262
+ rescue Aws::ECS::Errors::ClientException
263
+ raise if register_retry_count > 2
264
+ register_retry_count += 1
265
+ sleep 2
266
+ retry
267
+ end
172
268
  end
173
269
 
174
270
  def client
@@ -198,13 +294,14 @@ module Wrapbox
198
294
  value: cluster || self.cluster,
199
295
  },
200
296
  ],
297
+ timestamp: Time.now,
201
298
  value: 1.0,
202
299
  unit: "Count",
203
300
  ]
204
301
  )
205
302
  end
206
303
 
207
- def build_run_task_options(class_name, method_name, args, command, environments, cluster, task_definition_arn, task_role_arn)
304
+ def build_run_task_options(task_definition_arn, class_name, method_name, args, command, cluster, environments, task_role_arn)
208
305
  env = environments
209
306
  env += [
210
307
  {
@@ -252,12 +349,9 @@ module Wrapbox
252
349
  method_option :timeout, type: :numeric
253
350
  method_option :launch_timeout, type: :numeric
254
351
  method_option :launch_retry, type: :numeric
352
+ method_option :execution_retry, type: :numeric
255
353
  method_option :max_retry_interval, type: :numeric
256
354
  def run_cmd(*args)
257
- if args.size == 1
258
- args = args[0].split(" ")
259
- end
260
-
261
355
  repo = Wrapbox::ConfigRepository.new.tap { |r| r.load_yaml(options[:config]) }
262
356
  config = repo.get(options[:config_name])
263
357
  config.runner = :ecs
@@ -270,9 +364,10 @@ module Wrapbox
270
364
  timeout: options[:timeout],
271
365
  launch_timeout: options[:launch_timeout],
272
366
  launch_retry: options[:launch_retry],
367
+ execution_retry: options[:execution_retry],
273
368
  max_retry_interval: options[:max_retry_interval]
274
369
  }.reject { |_, v| v.nil? }
275
- runner.run_cmd(*args, environments: environments, **run_options)
370
+ runner.run_cmd(args, environments: environments, **run_options)
276
371
  end
277
372
  end
278
373
  end
@@ -1,3 +1,3 @@
1
1
  module Wrapbox
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wrapbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-02-17 00:00:00.000000000 Z
11
+ date: 2017-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk