gizzmo 0.11.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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