gizzmo 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ module Gizzard
2
+ module Transformation::Op
3
+ class BaseOp
4
+ def inverse?(other)
5
+ Transformation::OP_INVERSES[self.class] == other.class
6
+ end
7
+
8
+ def eql?(other)
9
+ self.class == other.class
10
+ end
11
+
12
+ alias == eql?
13
+
14
+ def inspect
15
+ templates = (is_a?(LinkOp) ? [from, to] : [template]).map {|t| t.identifier }.join(" -> ")
16
+ name = Transformation::OP_NAMES[self.class]
17
+ "#{name}(#{templates})"
18
+ end
19
+
20
+ def <=>(other)
21
+ Transformation::OP_PRIORITIES[self.class] <=> Transformation::OP_PRIORITIES[other.class]
22
+ end
23
+
24
+ def involved_shards(table_prefix, translations)
25
+ []
26
+ end
27
+ end
28
+
29
+ class CopyShard < BaseOp
30
+ BUSY = 1
31
+
32
+ attr_reader :from, :to
33
+ alias template to
34
+
35
+ def initialize(from, to)
36
+ @from = from
37
+ @to = to
38
+ end
39
+
40
+ def expand(*args); { :copy => [self] } end
41
+
42
+ def involved_shards(table_prefix, translations)
43
+ [to.to_shard_id(table_prefix, translations)]
44
+ end
45
+
46
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
47
+ from_shard_id = from.to_shard_id(table_prefix, translations)
48
+ to_shard_id = to.to_shard_id(table_prefix, translations)
49
+
50
+ nameserver.mark_shard_busy(to_shard_id, BUSY)
51
+ nameserver.copy_shard(from_shard_id, to_shard_id)
52
+ end
53
+ end
54
+
55
+ class LinkOp < BaseOp
56
+ attr_reader :from, :to
57
+ alias template to
58
+
59
+ def initialize(from, to)
60
+ @from = from
61
+ @to = to
62
+ end
63
+
64
+ def inverse?(other)
65
+ super && self.from.link_eql?(other.from) && self.to.link_eql?(other.to)
66
+ end
67
+
68
+ def eql?(other)
69
+ super && self.from.link_eql?(other.from) && self.to.link_eql?(other.to)
70
+ end
71
+ end
72
+
73
+ class AddLink < LinkOp
74
+ def expand(copy_source, involved_in_copy, wrapper_type)
75
+ if involved_in_copy
76
+ wrapper = ShardTemplate.new(wrapper_type, to.host, to.weight, '', '', [to])
77
+ { :prepare => [AddLink.new(from, wrapper)],
78
+ :cleanup => [self, RemoveLink.new(from, wrapper)] }
79
+ else
80
+ { :prepare => [self] }
81
+ end
82
+ end
83
+
84
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
85
+ from_shard_id = from.to_shard_id(table_prefix, translations)
86
+ to_shard_id = to.to_shard_id(table_prefix, translations)
87
+
88
+ nameserver.add_link(from_shard_id, to_shard_id, to.weight)
89
+ end
90
+ end
91
+
92
+ class RemoveLink < LinkOp
93
+ def expand(copy_source, involved_in_copy, wrapper_type)
94
+ { (involved_in_copy ? :cleanup : :prepare) => [self] }
95
+ end
96
+
97
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
98
+ from_shard_id = from.to_shard_id(table_prefix, translations)
99
+ to_shard_id = to.to_shard_id(table_prefix, translations)
100
+
101
+ nameserver.remove_link(from_shard_id, to_shard_id)
102
+ end
103
+ end
104
+
105
+ class ShardOp < BaseOp
106
+ attr_reader :template
107
+
108
+ def initialize(template)
109
+ @template = template
110
+ end
111
+
112
+ def inverse?(other)
113
+ super && self.template.shard_eql?(other.template)
114
+ end
115
+
116
+ def eql?(other)
117
+ super && self.template.shard_eql?(other.template)
118
+ end
119
+ end
120
+
121
+ class CreateShard < ShardOp
122
+ def expand(copy_source, involved_in_copy, wrapper_type)
123
+ if involved_in_copy
124
+ wrapper = ShardTemplate.new(wrapper_type, template.host, template.weight, '', '', [template])
125
+ { :prepare => [self, CreateShard.new(wrapper), AddLink.new(wrapper, template)],
126
+ :cleanup => [RemoveLink.new(wrapper, template), DeleteShard.new(wrapper)],
127
+ :copy => [CopyShard.new(copy_source, template)] }
128
+ else
129
+ { :prepare => [self] }
130
+ end
131
+ end
132
+
133
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
134
+ nameserver.create_shard(template.to_shard_info(table_prefix, translations))
135
+ end
136
+ end
137
+
138
+ class DeleteShard < ShardOp
139
+ def expand(copy_source, involved_in_copy, wrapper_type)
140
+ { (involved_in_copy ? :cleanup : :prepare) => [self] }
141
+ end
142
+
143
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
144
+ nameserver.delete_shard(template.to_shard_id(table_prefix, translations))
145
+ end
146
+ end
147
+
148
+ class SetForwarding < ShardOp
149
+ def expand(copy_source, involved_in_copy, wrapper_type)
150
+ if involved_in_copy
151
+ wrapper = ShardTemplate.new(wrapper_type, nil, 0, '', '', [to])
152
+ { :prepare => [SetForwarding.new(template, wrapper)],
153
+ :cleanup => [self] }
154
+ else
155
+ { :prepare => [self] }
156
+ end
157
+ end
158
+
159
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
160
+ shard_id = template.to_shard_id(table_prefix, translations)
161
+ forwarding = Forwarding.new(table_id, base_id, shard_id)
162
+ nameserver.set_forwarding(forwarding)
163
+ end
164
+ end
165
+
166
+
167
+ # XXX: A no-op, but needed for setup/teardown symmetry
168
+
169
+ class RemoveForwarding < ShardOp
170
+ def expand(copy_source, involved_in_copy, wrapper_type)
171
+ { (involved_in_copy ? :cleanup : :prepare) => [self] }
172
+ end
173
+
174
+ def apply(nameserver, table_id, base_id, table_prefix, translations)
175
+ # shard_id = template.to_shard_id(table_prefix, translations)
176
+ # forwarding = Forwarding.new(table_id, base_id, shard_id)
177
+ # nameserver.remove_forwarding(forwarding)
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,220 @@
1
+ module Gizzard
2
+ def self.schedule!(*args)
3
+ Transformation::Scheduler.new(*args).apply!
4
+ end
5
+
6
+ class Transformation::Scheduler
7
+
8
+ attr_reader :nameserver, :transformations
9
+ attr_reader :max_copies, :copies_per_host
10
+
11
+ DEFAULT_OPTIONS = {
12
+ :max_copies => 30,
13
+ :copies_per_host => 8,
14
+ :poll_interval => 10
15
+ }.freeze
16
+
17
+ def initialize(nameserver, base_name, transformations, options = {})
18
+ options = DEFAULT_OPTIONS.merge(options)
19
+ @nameserver = nameserver
20
+ @transformations = transformations
21
+ @max_copies = options[:max_copies]
22
+ @copies_per_host = options[:copies_per_host]
23
+ @poll_interval = options[:poll_interval]
24
+ @be_quiet = options[:quiet]
25
+ @dont_show_progress = options[:no_progress] || @be_quiet
26
+
27
+ @jobs_in_progress = []
28
+ @jobs_finished = []
29
+
30
+ @jobs_pending = transformations.map do |transformation, forwardings_to_shards|
31
+ transformation.bind(base_name, forwardings_to_shards)
32
+ end.flatten
33
+ end
34
+
35
+ # to schedule a job:
36
+ # 1. pull a job that does not involve a disqualified host.
37
+ # 2. run prepare ops
38
+ # 3. reload app servers
39
+ # 4. schedule copy
40
+ # 5. put in jobs_in_progress
41
+
42
+ # on job completion:
43
+ # 1. run cleanup ops
44
+ # 2. remove from jobs_in_progress
45
+ # 3. put in jos_finished
46
+ # 4. schedule a new job or reload app servers.
47
+
48
+ def apply!
49
+ @start_time = Time.now
50
+
51
+ loop do
52
+ reload_busy_shards
53
+
54
+ cleanup_jobs
55
+ schedule_jobs(max_copies - busy_shards.length)
56
+
57
+ break if @jobs_pending.empty? && @jobs_in_progress.empty?
58
+
59
+ unless nameserver.dryrun?
60
+ if @dont_show_progress
61
+ sleep(@poll_interval)
62
+ else
63
+ sleep_with_progress(@poll_interval)
64
+ end
65
+ end
66
+ end
67
+
68
+ nameserver.reload_config
69
+
70
+ log "#{@jobs_finished.length} transformation#{'s' if @jobs_finished.length > 1} applied. Total time elapsed: #{time_elapsed}"
71
+ end
72
+
73
+ def schedule_jobs(num_to_schedule)
74
+ to_be_busy_hosts = []
75
+
76
+ jobs = (1..num_to_schedule).map do
77
+ job = @jobs_pending.find do |j|
78
+ (busy_hosts(to_be_busy_hosts) & j.involved_hosts).empty?
79
+ end
80
+
81
+ if job
82
+ to_be_busy_hosts.concat job.involved_hosts
83
+ @jobs_pending.delete(job)
84
+ end
85
+
86
+ job
87
+ end.compact.sort_by {|t| t.forwarding }
88
+
89
+ unless jobs.empty?
90
+ log "STARTING:"
91
+ jobs.each do |j|
92
+ log " #{j.inspect}"
93
+ j.prepare!(nameserver)
94
+ end
95
+
96
+ nameserver.reload_config
97
+
98
+ copy_jobs = jobs.select {|j| j.copy_required? }
99
+
100
+ unless copy_jobs.empty?
101
+ log "COPIES:"
102
+ copy_jobs.each do |j|
103
+ j.copy_descs.each {|d| log " #{d}" }
104
+ j.copy!(nameserver)
105
+ end
106
+
107
+ reload_busy_shards
108
+ end
109
+
110
+ @jobs_in_progress.concat(jobs)
111
+ end
112
+ end
113
+
114
+ def cleanup_jobs
115
+ jobs = jobs_completed
116
+
117
+ unless jobs.empty?
118
+ @jobs_in_progress -= jobs
119
+
120
+ log "FINISHING:"
121
+ jobs.each do |j|
122
+ log " #{j.inspect}"
123
+ j.cleanup!(nameserver)
124
+ end
125
+
126
+ @jobs_finished.concat(jobs)
127
+ end
128
+ end
129
+
130
+ def jobs_completed
131
+ @jobs_in_progress.select {|j| (busy_shards & j.involved_shards).empty? }
132
+ end
133
+
134
+ def reload_busy_shards
135
+ @busy_shards = nil
136
+ busy_shards
137
+ end
138
+
139
+ def busy_shards
140
+ @busy_shards ||=
141
+ if nameserver.dryrun?
142
+ []
143
+ else
144
+ nameserver.get_busy_shards.map {|s| s.id }
145
+ end
146
+ end
147
+
148
+ def busy_hosts(extra_hosts = [])
149
+ hosts = extra_hosts + busy_shards.map {|s| s.hostname }
150
+
151
+ copies_count_map = hosts.inject({}) do |h, host|
152
+ h.update(host => 1) {|_,a,b| a + b }
153
+ end
154
+
155
+ copies_count_map.select {|_, count| count >= @copies_per_host }.map {|(host, _)| host }
156
+ end
157
+
158
+ def sleep_with_progress(interval)
159
+ start = Time.now
160
+ while (Time.now - start) < interval
161
+ put_copy_progress
162
+ sleep 0.2
163
+ end
164
+ end
165
+
166
+ def clear_progress_string
167
+ if @progress_string
168
+ print "\r" + (" " * (@progress_string.length + 10)) + "\r"
169
+ @progress_string = nil
170
+ end
171
+ end
172
+
173
+ def log(*args)
174
+ unless @be_quiet
175
+ clear_progress_string
176
+ puts *args
177
+ end
178
+ end
179
+
180
+ def put_copy_progress
181
+ @i ||= 0
182
+ @i += 1
183
+
184
+ unless @jobs_in_progress.empty? || busy_shards.empty?
185
+ spinner = ['-', '\\', '|', '/'][@i % 4]
186
+ elapsed_txt = "Time elapsed: #{time_elapsed}"
187
+ pending_txt = "Pending: #{@jobs_pending.length}"
188
+ finished_txt = "Finished: #{@jobs_finished.length}"
189
+ in_progress_txt =
190
+ if busy_shards.length != @jobs_in_progress.length
191
+ "In progress: #{@jobs_in_progress.length} (Copies: #{busy_shards.length})"
192
+ else
193
+ "In progress: #{@jobs_in_progress.length}"
194
+ end
195
+
196
+ clear_progress_string
197
+
198
+ @progress_string = "#{spinner} #{in_progress_txt} #{pending_txt} #{finished_txt} #{elapsed_txt}"
199
+ print @progress_string; $stdout.flush
200
+ end
201
+ end
202
+
203
+ def time_elapsed
204
+ s = (Time.now - @start_time).to_i
205
+
206
+ if s == 1
207
+ "1 second"
208
+ elsif s < 60
209
+ "#{s} seconds"
210
+ else
211
+ days = s / (60 * 60 * 24) if s >= 60 * 60 * 24
212
+ hours = (s % (60 * 60 * 24)) / (60 * 60) if s >= 60 * 60
213
+ minutes = (s % (60 * 60)) / 60 if s >= 60
214
+ seconds = s % 60
215
+
216
+ [days,hours,minutes,seconds].compact.map {|i| "%0.2i" % i }.join(":")
217
+ end
218
+ end
219
+ end
220
+ end
data/lib/gizzmo.rb CHANGED
@@ -22,7 +22,7 @@ DOC_STRINGS = {
22
22
  "lookup" => "Lookup the shard id that holds the record for a given table / source_id.",
23
23
  "markbusy" => "Mark a shard as busy.",
24
24
  "pair" => "Report the replica pairing structure for a list of hosts.",
25
- "reload" => "Instruct an appserver to reload its nameserver state.",
25
+ "reload" => "Instruct application servers to reload the nameserver state.",
26
26
  "report" => "Show each unique replica structure for a given list of shards. Usually this shard list comes from << gizzmo forwardings | awk '{ print $3 }' >>.",
27
27
  "setup-replica" => "Add a replica to be parallel to an existing replica, in write-only mode, ready to be copied to.",
28
28
  "wrap" => "Wrapping creates a new (virtual, e.g. blocking, replicating, etc.) shard, and relinks SHARD_ID_TO_WRAP's parent links to run through the new shard.",
@@ -32,9 +32,12 @@ ORIGINAL_ARGV = ARGV.dup
32
32
  zero = File.basename($0)
33
33
 
34
34
  # Container for parsed options
35
- global_options = OpenStruct.new
36
- global_options.render = []
37
- global_options.framed = false
35
+ global_options = OpenStruct.new
36
+ global_options.port = 7920
37
+ global_options.injector_port = 7921
38
+ global_options.render = []
39
+ global_options.framed = false
40
+
38
41
  subcommand_options = OpenStruct.new
39
42
 
40
43
  # Leftover arguments
@@ -78,10 +81,29 @@ end
78
81
 
79
82
  def load_config(options, filename)
80
83
  YAML.load(File.open(filename)).each do |k, v|
84
+ v = v.split(",").map {|h| h.strip } if k == "hosts"
81
85
  options.send("#{k}=", v)
82
86
  end
83
87
  end
84
88
 
89
+ def add_scheduler_opts(subcommand_options, opts)
90
+ opts.on("--max-copies=COUNT", "Limit max simultaneous copies to COUNT.") do |c|
91
+ (subcommand_options.scheduler_options ||= {})[:max_copies] = c.to_i
92
+ end
93
+ opts.on("--copies-per-host=COUNT", "Limit max copies per individual destination host to COUNT") do |c|
94
+ (subcommand_options.scheduler_options ||= {})[:copies_per_host] = c.to_i
95
+ end
96
+ opts.on("--poll-interval=SECONDS", "Sleep SECONDS between polling for copy status") do |c|
97
+ (subcommand_options.scheduler_options ||= {})[:poll_interval] = c.to_i
98
+ end
99
+ opts.on("--copy-wrapper=TYPE", "Wrap copy destination shards with TYPE. default WriteOnlyShard") do |t|
100
+ (subcommand_options.scheduler_options ||= {})[:copy_wrapper] = t
101
+ end
102
+ opts.on("--no-progress", "Do not show progress bar at bottom.") do
103
+ (subcommand_options.scheduler_options ||= {})[:no_progress] = true
104
+ end
105
+ end
106
+
85
107
  subcommands = {
86
108
  'create' => OptionParser.new do |opts|
87
109
  opts.banner = "Usage: #{zero} create [options] CLASS_NAME SHARD_ID [MORE SHARD_IDS...]"
@@ -106,7 +128,7 @@ subcommands = {
106
128
  opts.on("-w", "--write-only=CLASS") do |w|
107
129
  subcommand_options.write_only_shard = w
108
130
  end
109
- opts.on("-h", "--hosts=list") do |h|
131
+ opts.on("-h", "--shard-hosts=list") do |h|
110
132
  subcommand_options.hosts = h
111
133
  end
112
134
  opts.on("-x", "--exclude-hosts=list") do |x|
@@ -149,10 +171,6 @@ subcommands = {
149
171
  opts.banner = "Usage: #{zero} addforwarding TABLE_ID BASE_ID SHARD_ID"
150
172
  separators(opts, DOC_STRINGS["addforwarding"])
151
173
  end,
152
- 'currentforwarding' => OptionParser.new do |opts|
153
- opts.banner = "Usage: #{zero} currentforwarding SOURCE_ID [ANOTHER_SOURCE_ID...]"
154
- separators(opts, DOC_STRINGS["addforwarding"])
155
- end,
156
174
  'forwardings' => OptionParser.new do |opts|
157
175
  opts.banner = "Usage: #{zero} forwardings [options]"
158
176
  separators(opts, DOC_STRINGS["forwardings"])
@@ -177,7 +195,7 @@ subcommands = {
177
195
  subcommand_options.shard_type = shard_type
178
196
  end
179
197
 
180
- opts.on("-H", "--host=HOST", "HOST of shard") do |shard_host|
198
+ opts.on("-h", "--shard-host=HOST", "HOST of shard") do |shard_host|
181
199
  subcommand_options.shard_host = shard_host
182
200
  end
183
201
  end,
@@ -267,6 +285,46 @@ subcommands = {
267
285
  opts.on("--all", "Flush all error queues.") do
268
286
  subcommand_options.flush_all = true
269
287
  end
288
+ end,
289
+ 'add-host' => OptionParser.new do |opts|
290
+ opts.banner = "Usage: #{zero} add-host HOSTS"
291
+ separators(opts, DOC_STRINGS["add-host"])
292
+ end,
293
+ 'remove-host' => OptionParser.new do |opts|
294
+ opts.banner = "Usage: #{zero} remove-host HOST"
295
+ separators(opts, DOC_STRINGS["remove-host"])
296
+ end,
297
+ 'list-hosts' => OptionParser.new do |opts|
298
+ opts.banner = "Usage: #{zero} list-hosts"
299
+ separators(opts, DOC_STRINGS["list-hosts"])
300
+ end,
301
+ 'topology' => OptionParser.new do |opts|
302
+ opts.banner = "Usage: #{zero} topology [options]"
303
+ separators(opts, DOC_STRINGS["topology"])
304
+
305
+ opts.on("--forwardings", "Show topology of forwardings instead of counts") do
306
+ subcommand_options.forwardings = true
307
+ end
308
+ end,
309
+ 'transform-tree' => OptionParser.new do |opts|
310
+ opts.banner = "Usage: #{zero} transform-tree [options] ROOT_SHARD_ID TEMPLATE"
311
+ separators(opts, DOC_STRINGS['transform-tree'])
312
+
313
+ add_scheduler_opts subcommand_options, opts
314
+
315
+ opts.on("-q", "--quiet", "Do not display transformation info (only valid with --force)") do
316
+ subcommand_options.quiet = true
317
+ end
318
+ end,
319
+ 'transform' => OptionParser.new do |opts|
320
+ opts.banner = "Usage: #{zero} transform [options] FROM_TEMPLATE TO_TEMPLATE"
321
+ separators(opts, DOC_STRINGS['transform'])
322
+
323
+ add_scheduler_opts subcommand_options, opts
324
+
325
+ opts.on("-q", "--quiet", "Do not display transformation info (only valid with --force)") do
326
+ subcommand_options.quiet = true
327
+ end
270
328
  end
271
329
  }
272
330
 
@@ -288,7 +346,7 @@ global = OptionParser.new do |opts|
288
346
  opts.separator "key/value pairs corresponding to options you want by default. A common .gizzmorc"
289
347
  opts.separator "simply contains:"
290
348
  opts.separator ""
291
- opts.separator " host: localhost"
349
+ opts.separator " hosts: localhost"
292
350
  opts.separator " port: 7917"
293
351
  opts.separator ""
294
352
  opts.separator "Subcommands:"
@@ -305,24 +363,32 @@ global = OptionParser.new do |opts|
305
363
  opts.separator ""
306
364
  opts.separator ""
307
365
  opts.separator "Global options:"
308
- opts.on("-H", "--host=HOSTNAME", "HOSTNAME of remote thrift service") do |host|
309
- global_options.host = host
366
+ opts.on("-H", "--hosts=HOST[,HOST,...]", "HOSTS of application servers") do |hosts|
367
+ global_options.hosts = hosts.split(",").map {|h| h.strip }
310
368
  end
311
369
 
312
- opts.on("-P", "--port=PORT", "PORT of remote thrift service") do |port|
370
+ opts.on("-P", "--port=PORT", "PORT of remote manager service. default 7920") do |port|
313
371
  global_options.port = port.to_i
314
372
  end
315
373
 
374
+ opts.on("-I", "--injector=PORT", "PORT of remote job injector service. default 7921") do |port|
375
+ global_options.injector_port = port.to_i
376
+ end
377
+
378
+ opts.on("-T", "--tables=TABLE[,TABLE,...]", "TABLE ids of forwardings to affect") do |tables|
379
+ global_options.tables = tables.split(",").map {|t| t.to_i }
380
+ end
381
+
316
382
  opts.on("-F", "--framed", "use the thrift framed transport") do |framed|
317
383
  global_options.framed = true
318
384
  end
319
385
 
320
386
  opts.on("-r", "--retry=TIMES", "TIMES to retry the command") do |r|
321
- global_options.retry = r
387
+ global_options.retry = r.to_i
322
388
  end
323
389
 
324
390
  opts.on("-t", "--timeout=SECONDS", "SECONDS to let the command run") do |r|
325
- global_options.timeout = r
391
+ global_options.timeout = r.to_i
326
392
  end
327
393
 
328
394
  opts.on("--subtree", "Render in subtree mode") do
@@ -333,7 +399,7 @@ global = OptionParser.new do |opts|
333
399
  global_options.render << "info"
334
400
  end
335
401
 
336
- opts.on("-D", "--dry-run", "") do |port|
402
+ opts.on("-D", "--dry-run", "") do
337
403
  global_options.dry = true
338
404
  end
339
405
 
@@ -401,12 +467,12 @@ def custom_timeout(seconds)
401
467
  begin
402
468
  require "rubygems"
403
469
  require "system_timer"
404
- SystemTimer.timeout_after(seconds.to_i) do
470
+ SystemTimer.timeout_after(seconds) do
405
471
  yield
406
472
  end
407
473
  rescue LoadError
408
474
  require "timeout"
409
- Timeout.timeout(seconds.to_i) do
475
+ Timeout.timeout(seconds) do
410
476
  yield
411
477
  end
412
478
  end
@@ -427,8 +493,9 @@ rescue HelpNeededError => e
427
493
  end
428
494
  STDERR.puts subcommands[subcommand_name]
429
495
  exit 1
430
- rescue ThriftClient::Simple::ThriftException, Gizzard::Thrift::ShardException, Errno::ECONNREFUSED => e
496
+ rescue ThriftClient::Simple::ThriftException, Gizzard::GizzardException, Errno::ECONNREFUSED => e
431
497
  STDERR.puts e.message
498
+ STDERR.puts e.backtrace
432
499
  exit 1
433
500
  rescue Errno::EPIPE
434
501
  # This is just us trying to puts into a closed stdout. For example, if you pipe into