Skip to content

Commit

Permalink
Add Crystal.print_buffered(io) and Crystal.print_error_buffered (#15343)
Browse files Browse the repository at this point in the history
Writes a message to a growable, in-memory buffer, before writing to a
standard IO (e.g. STDERR) in a single write (possibly atomic, depending
on PIPE_BUF) instead of having many individual writes to the IO which
will be intermingled with other writes and be completely unintelligible.
  • Loading branch information
ysbaddaden authored Jan 15, 2025
1 parent 5b20837 commit 7135b1d
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 12 deletions.
42 changes: 42 additions & 0 deletions src/crystal/print_buffered.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Crystal
# Prepares an error message, with an optional exception or backtrace, to an
# in-memory buffer, before writing to an IO, usually STDERR, in a single write
# operation.
#
# Avoids intermingled messages caused by multiple threads writing to a STDIO
# in parallel. This may still happen, since writes may not be atomic when the
# overall size is larger than PIPE_BUF, buf it should at least write 512 bytes
# atomically.
def self.print_buffered(message : String, *args, to io : IO, exception = nil, backtrace = nil) : Nil
buf = buffered_message(message, *args, exception: exception, backtrace: backtrace)
io.write(buf.to_slice)
io.flush unless io.sync?
end

# Identical to `#print_buffered` but eventually calls `System.print_error(bytes)`
# to write to stderr without going through the event loop.
def self.print_error_buffered(message : String, *args, exception = nil, backtrace = nil) : Nil
buf = buffered_message(message, *args, exception: exception, backtrace: backtrace)
System.print_error(buf.to_slice)
end

private def self.buffered_message(message : String, *args, exception = nil, backtrace = nil)
buf = IO::Memory.new(4096)

if args.empty?
buf << message
else
System.printf(message, *args) { |bytes| buf.write(bytes) }
end

if exception
buf << ": "
exception.inspect_with_backtrace(buf)
else
buf.puts
backtrace.try(&.each { |line| buf << " from " << line << '\n' })
end

buf
end
end
15 changes: 3 additions & 12 deletions src/fiber.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "crystal/system/thread_linked_list"
require "crystal/print_buffered"
require "./fiber/context"

# :nodoc:
Expand Down Expand Up @@ -147,21 +148,11 @@ class Fiber
GC.unlock_read
@proc.call
rescue ex
io = {% if flag?(:preview_mt) %}
IO::Memory.new(4096) # PIPE_BUF
{% else %}
STDERR
{% end %}
if name = @name
io << "Unhandled exception in spawn(name: " << name << "): "
Crystal.print_buffered("Unhandled exception in spawn(name: %s)", name, exception: ex, to: STDERR)
else
io << "Unhandled exception in spawn: "
Crystal.print_buffered("Unhandled exception in spawn", exception: ex, to: STDERR)
end
ex.inspect_with_backtrace(io)
{% if flag?(:preview_mt) %}
STDERR.write(io.to_slice)
{% end %}
STDERR.flush
ensure
# Remove the current fiber from the linked list
Fiber.inactive(self)
Expand Down

0 comments on commit 7135b1d

Please sign in to comment.