Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit cfe0448

Browse filesBrowse files
committed
EngineClient: remove stream on broken pipe
On write EPIPE, remove the stream from the engine at the point of error. The select engine has no POLLERR equivalent, so the stream was never removed and task.run() would not return. The poll and epoll engines were not affected, as they remove the stream when POLLERR is received, one loop iteration later; that path is kept as a fallback. Drop writes to an unknown or closed stream, like set_write_eof() already does, as the stream may now be removed while an event handler is still running. Add StreamEngineSelectTest to run StreamWorker tests under the select engine. Signed-off-by: Stephane Thiell <sthiell@stanford.edu>
1 parent 381842e commit cfe0448
Copy full SHA for cfe0448

2 files changed

+68-1Lines changed: 68 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎lib/ClusterShell/Worker/EngineClient.py‎

Copy file name to clipboardExpand all lines: lib/ClusterShell/Worker/EngineClient.py
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,9 @@ def _handle_write(self, sname):
339339
if exc.errno == errno.EPIPE:
340340
# broken pipe: log warning message and do NOT retry
341341
LOGGER.warning('%r: %s', self, exc)
342+
# remove stream now; select engine has no POLLERR
343+
if self._engine:
344+
self._engine.remove_stream(self, wfile)
342345
return
343346
raise
344347
if wcnt > 0:
@@ -410,6 +413,11 @@ def _readlines(self, sname):
410413

411414
def _write(self, sname, buf):
412415
"""Add some data to be written to the client."""
416+
if sname not in self.streams:
417+
LOGGER.debug("dropping write to unknown or closed stream %s on "
418+
"client %s", sname, self.key)
419+
return
420+
413421
wfile = self.streams[sname]
414422
if self._engine and wfile.fd:
415423
wfile.wbuf += buf
Collapse file

‎tests/StreamWorkerTest.py‎

Copy file name to clipboardExpand all lines: tests/StreamWorkerTest.py
+60-1Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import os
66
import unittest
77

8+
from ClusterShell.Defaults import DEFAULTS
9+
from ClusterShell.Engine.Select import EngineSelect
810
from ClusterShell.Worker.Worker import StreamWorker, WorkerError
9-
from ClusterShell.Task import task_self
11+
from ClusterShell.Task import task_self, task_terminate
1012
from ClusterShell.Event import EventHandler
1113

1214

@@ -369,3 +371,60 @@ def ev_read(self, worker, node, sname, msg):
369371
self.assertEqual(hdlr.read_count, 1) # single line only
370372
os.close(rfd1)
371373
os.close(wfd1)
374+
375+
def test_011_broken_pipe_on_write_twice(self):
376+
"""test StreamWorker with broken pipe and subsequent writes"""
377+
378+
# This test is similar to test_008 but performs more write() calls
379+
# after the broken pipe error to check they are safely dropped.
380+
381+
class TestH(EventHandler):
382+
def __init__(self, testcase, rfd):
383+
self.testcase = testcase
384+
self.rfd = rfd
385+
self.check_hup = 0
386+
self.check_written = 0
387+
388+
def ev_hup(self, worker, node, rc):
389+
self.check_hup += 1
390+
391+
def ev_written(self, worker, node, sname, size):
392+
self.check_written += 1
393+
self.testcase.assertEqual(os.read(self.rfd, 1024), b"initial")
394+
# close reader, that will stop the StreamWorker
395+
os.close(self.rfd)
396+
worker.write(b"final")
397+
# stream may be closed at this point; check that subsequent
398+
# writes are dropped without error
399+
worker.write(b"more", "test")
400+
worker.set_write_eof()
401+
402+
rfd, wfd = os.pipe()
403+
404+
hdlr = TestH(self, rfd)
405+
worker = StreamWorker(handler=hdlr)
406+
407+
worker.set_writer("test", wfd) # closefd=True
408+
worker.write(b"initial", "test")
409+
410+
self.run_worker(worker)
411+
self.assertEqual(hdlr.check_hup, 1)
412+
self.assertEqual(hdlr.check_written, 1)
413+
414+
415+
class StreamEngineSelectTest(StreamTest):
416+
"""run all StreamTest tests under the select engine"""
417+
418+
def setUp(self):
419+
# switch Engine
420+
task_terminate()
421+
self.engine_id_save = DEFAULTS.engine
422+
DEFAULTS.engine = EngineSelect.identifier
423+
# select should be supported anywhere...
424+
self.assertEqual(task_self().info('engine'),
425+
EngineSelect.identifier)
426+
427+
def tearDown(self):
428+
# restore Engine
429+
DEFAULTS.engine = self.engine_id_save
430+
task_terminate()

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.