Skip to content

Commit d727d2b

Browse files
Error handling vibe coding.
1 parent 4114372 commit d727d2b

2 files changed

Lines changed: 265 additions & 5 deletions

File tree

lib/async/bus/protocol/transaction.rb

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,20 @@ def invoke(name, arguments, options, &block)
120120
def accept(object, arguments, options, block_given)
121121
if block_given
122122
result = object.public_send(*arguments, **options) do |*yield_arguments|
123-
self.write(Yield.new(@id, yield_arguments))
123+
begin
124+
self.write(Yield.new(@id, yield_arguments))
125+
rescue IOError, EOFError, RuntimeError => error
126+
# Connection closed or transaction closed - can't send yield
127+
# Break out of the iteration since we can't communicate
128+
break
129+
end
124130

125-
response = self.read
131+
begin
132+
response = self.read
133+
rescue ClosedQueueError, IOError, EOFError
134+
# Connection closed - can't receive response
135+
break
136+
end
126137

127138
case response
128139
when Next
@@ -137,13 +148,28 @@ def accept(object, arguments, options, block_given)
137148
result = object.public_send(*arguments, **options)
138149
end
139150

140-
self.write(Return.new(@id, result))
151+
begin
152+
self.write(Return.new(@id, result))
153+
rescue IOError, EOFError, RuntimeError => error
154+
# Connection closed or transaction closed - can't send return
155+
# This is expected when connection terminates mid-transaction
156+
end
141157
rescue UncaughtThrowError => error
142158
# UncaughtThrowError has both tag and value attributes
143159
# Store both in the Throw message: result is tag, we'll add value handling
144-
self.write(Throw.new(@id, [error.tag, error.value]))
160+
begin
161+
self.write(Throw.new(@id, [error.tag, error.value]))
162+
rescue IOError, EOFError, RuntimeError
163+
# Connection closed or transaction closed - can't send throw
164+
# This is expected when connection terminates mid-transaction
165+
end
145166
rescue => error
146-
self.write(Error.new(@id, error))
167+
begin
168+
self.write(Error.new(@id, error))
169+
rescue IOError, EOFError, RuntimeError
170+
# Connection closed or transaction closed - can't send error
171+
# This is expected when connection terminates mid-transaction
172+
end
147173
end
148174
end
149175
end

test/async/bus/protocol/transaction.rb

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,5 +237,239 @@ def service.throw_method
237237
end
238238
end
239239
end
240+
241+
with "connection termination during transaction" do
242+
it "demonstrates unhandled RuntimeError when server crashes while client waits for response" do
243+
# This test demonstrates that when a server connection is closed while
244+
# a transaction is in-flight, the server task tries to write Return/Error
245+
# but the transaction is already closed, causing an unhandled RuntimeError.
246+
#
247+
# Expected behavior: When the remote end closes the connection mid-transaction,
248+
# the local end should fail with an exception, but that exception should be
249+
# handled gracefully (not left as an unhandled exception in an async task).
250+
server_connection = nil
251+
server_started = Thread::Queue.new
252+
close_connection = Thread::Queue.new
253+
254+
start_server do |connection|
255+
server_connection = connection
256+
service = Object.new
257+
def service.slow_method
258+
# Signal that server has started processing
259+
@server_started.push(:started)
260+
# Wait for signal to close connection
261+
@close_connection.pop
262+
:result
263+
end
264+
265+
# Store queue references in service
266+
service.instance_variable_set(:@server_started, server_started)
267+
service.instance_variable_set(:@close_connection, close_connection)
268+
269+
connection.bind(:service, service)
270+
end
271+
272+
client.connect do |connection|
273+
# Start the invocation
274+
invoke_task = Async do
275+
connection[:service].slow_method
276+
end
277+
278+
# Wait for server to start processing
279+
server_started.pop
280+
281+
# Abruptly close the server connection
282+
server_connection.close
283+
284+
# Signal server to continue (it will try to write Return/Error)
285+
close_connection.push(:close)
286+
287+
# Wait for processing to complete
288+
begin
289+
invoke_task.wait
290+
rescue
291+
# Expected to fail
292+
end
293+
294+
# Currently, this causes unhandled RuntimeError: "Transaction is closed!"
295+
# in the server task when it tries to write Return/Error after connection closes.
296+
# The test passes but demonstrates the issue: exceptions should be caught and
297+
# handled gracefully, not left as unhandled exceptions in async tasks.
298+
end
299+
end
300+
301+
it "demonstrates unhandled RuntimeError when client crashes while server executes method" do
302+
# This test demonstrates that when a client connection is closed while
303+
# a server transaction is executing, the server task tries to write Return/Error
304+
# but the transaction is already closed, causing an unhandled RuntimeError.
305+
#
306+
# Expected behavior: When the remote end closes the connection mid-transaction,
307+
# the local end should fail with an exception, but that exception should be
308+
# handled gracefully (not left as an unhandled exception in an async task).
309+
client_connection = nil
310+
server_started = Thread::Queue.new
311+
close_connection = Thread::Queue.new
312+
313+
start_server do |connection|
314+
service = Object.new
315+
def service.slow_method
316+
# Signal that server has started processing
317+
@server_started.push(:started)
318+
# Wait for signal to close connection
319+
@close_connection.pop
320+
:result
321+
end
322+
323+
# Store queue references in service
324+
service.instance_variable_set(:@server_started, server_started)
325+
service.instance_variable_set(:@close_connection, close_connection)
326+
327+
connection.bind(:service, service)
328+
end
329+
330+
client.connect do |connection|
331+
client_connection = connection
332+
333+
# Start the invocation
334+
invoke_task = Async do
335+
connection[:service].slow_method
336+
end
337+
338+
# Wait for server to start processing
339+
server_started.pop
340+
341+
# Abruptly close the client connection
342+
client_connection.close
343+
344+
# Signal server to continue (it will try to write Return/Error)
345+
close_connection.push(:close)
346+
347+
# Wait for processing to complete
348+
begin
349+
invoke_task.wait
350+
rescue
351+
# Expected to fail
352+
end
353+
354+
# Currently, this causes unhandled RuntimeError: "Transaction is closed!"
355+
# in the server task when it tries to write Return/Error after connection closes.
356+
# The test passes but demonstrates the issue: exceptions should be caught and
357+
# handled gracefully, not left as unhandled exceptions in async tasks.
358+
end
359+
end
360+
361+
it "demonstrates unhandled RuntimeError when server crashes during yield operation" do
362+
# This test demonstrates that when a server connection is closed during
363+
# a yield operation, the server task tries to write Yield but the transaction
364+
# is already closed, causing an unhandled RuntimeError.
365+
#
366+
# Expected behavior: When the remote end closes the connection mid-transaction,
367+
# the local end should fail with an exception, but that exception should be
368+
# handled gracefully (not left as an unhandled exception in an async task).
369+
server_connection = nil
370+
first_yield_received = Thread::Queue.new
371+
372+
start_server do |connection|
373+
server_connection = connection
374+
service = Object.new
375+
def service.yielding_method
376+
yield 1
377+
# Wait for connection to be closed
378+
@first_yield_received.pop
379+
# This yield will fail because connection closes
380+
yield 2
381+
:done
382+
end
383+
384+
# Store queue reference in service
385+
service.instance_variable_set(:@first_yield_received, first_yield_received)
386+
387+
connection.bind(:service, service)
388+
end
389+
390+
client.connect do |connection|
391+
# Start the invocation with yield
392+
invoke_task = Async do
393+
results = []
394+
connection[:service].yielding_method do |value|
395+
results << value
396+
397+
# Close server connection after first yield
398+
if results.size == 1
399+
server_connection.close
400+
first_yield_received.push(:received)
401+
end
402+
403+
:ack
404+
end
405+
end
406+
407+
# Wait for processing to complete
408+
begin
409+
invoke_task.wait
410+
rescue
411+
# Expected to fail
412+
end
413+
414+
# Currently, this causes unhandled RuntimeError: "Transaction is closed!"
415+
# in the server task when it tries to write Yield after connection closes.
416+
# The test passes but demonstrates the issue: exceptions should be caught and
417+
# handled gracefully, not left as unhandled exceptions in async tasks.
418+
end
419+
end
420+
421+
it "handles client crash during server yield operation" do
422+
client_connection = nil
423+
first_yield_received = Thread::Queue.new
424+
425+
start_server do |connection|
426+
service = Object.new
427+
def service.yielding_method
428+
yield 1
429+
# Wait for connection to be closed
430+
@first_yield_received.pop
431+
# This yield will fail because client closes
432+
yield 2
433+
:done
434+
end
435+
436+
# Store queue reference in service
437+
service.instance_variable_set(:@first_yield_received, first_yield_received)
438+
439+
connection.bind(:service, service)
440+
end
441+
442+
client.connect do |connection|
443+
client_connection = connection
444+
445+
# Start the invocation with yield
446+
invoke_task = Async do
447+
results = []
448+
connection[:service].yielding_method do |value|
449+
results << value
450+
451+
# Close client connection after first yield
452+
if results.size == 1
453+
client_connection.close
454+
first_yield_received.push(:received)
455+
end
456+
457+
:ack
458+
end
459+
end
460+
461+
# Wait for processing to complete
462+
begin
463+
invoke_task.wait
464+
rescue
465+
# Expected to fail
466+
end
467+
468+
# When the remote end closes the connection mid-transaction, the local end
469+
# should fail with an exception, but that exception should be handled gracefully
470+
# (not left as an unhandled exception in an async task).
471+
end
472+
end
473+
end
240474
end
241475

0 commit comments

Comments
 (0)