Skip to content

Commit 9ce5277

Browse files
authored
Add error boundaries to prevent write stream errors in InstigationLogger from failing schedule/sensor ticks (#26609)
This prevents issues with the compute log manager from failing a schedule or sensor tick in which the logging call is happening. ## Summary & Motivation ## How I Tested These Changes BK ## Changelog Exceptions that are raised when a schedule or sensor are writing logs will now write an error message to stdout instead of failing the tick. > Insert changelog entry or delete this section.
1 parent 3ea9100 commit 9ce5277

File tree

2 files changed

+90
-6
lines changed

2 files changed

+90
-6
lines changed

python_modules/dagster/dagster/_core/definitions/instigation_logger.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
import sys
34
import threading
45
import traceback
56
from contextlib import ExitStack
@@ -10,6 +11,7 @@
1011
from dagster._core.log_manager import LOG_RECORD_METADATA_ATTR
1112
from dagster._core.storage.compute_log_manager import ComputeIOType, ComputeLogManager
1213
from dagster._core.utils import coerce_valid_log_level
14+
from dagster._utils.error import serializable_error_info_from_exc_info
1315
from dagster._utils.log import create_console_logger
1416

1517

@@ -66,7 +68,12 @@ def emit(self, record: logging.LogRecord):
6668
if exc_info:
6769
record_dict["exc_info"] = "".join(traceback.format_exception(*exc_info))
6870

69-
self._write_stream.write(_seven.json.dumps(record_dict) + "\n")
71+
try:
72+
self._write_stream.write(_seven.json.dumps(record_dict) + "\n")
73+
except Exception:
74+
sys.stderr.write(
75+
f"Exception writing to logger event stream: {serializable_error_info_from_exc_info(sys.exc_info())}\n"
76+
)
7077

7178

7279
class InstigationLogger(logging.Logger):
@@ -107,18 +114,30 @@ def __enter__(self):
107114
and self._instance
108115
and isinstance(self._instance.compute_log_manager, ComputeLogManager)
109116
):
110-
write_stream = self._exit_stack.enter_context(
111-
self._instance.compute_log_manager.open_log_stream(
112-
self._log_key, ComputeIOType.STDERR
117+
try:
118+
write_stream = self._exit_stack.enter_context(
119+
self._instance.compute_log_manager.open_log_stream(
120+
self._log_key, ComputeIOType.STDERR
121+
)
113122
)
114-
)
123+
except Exception:
124+
sys.stderr.write(
125+
f"Exception initializing logger write stream: {serializable_error_info_from_exc_info(sys.exc_info())}\n"
126+
)
127+
write_stream = None
128+
115129
if write_stream:
116130
self._capture_handler = CapturedLogHandler(write_stream)
117131
self.addHandler(self._capture_handler)
118132
return self
119133

120134
def __exit__(self, _exception_type, _exception_value, _traceback):
121-
self._exit_stack.close()
135+
try:
136+
self._exit_stack.close()
137+
except Exception:
138+
sys.stderr.write(
139+
f"Exception closing logger write stream: {serializable_error_info_from_exc_info(sys.exc_info())}\n"
140+
)
122141

123142
def _annotate_record(self, record: logging.LogRecord) -> logging.LogRecord:
124143
if self._repository_name and self._instigator_name:

python_modules/dagster/dagster_tests/storage_tests/test_instigation_logger.py

+65
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
from contextlib import contextmanager
2+
from unittest import mock
3+
14
from dagster._core.definitions.instigation_logger import InstigationLogger
5+
from dagster._core.storage.noop_compute_log_manager import NoOpComputeLogManager
6+
from dagster._core.test_utils import instance_for_test
27

38

49
def test_gets_correct_logger():
@@ -9,3 +14,63 @@ def test_gets_correct_logger():
914

1015
instigation_logger = InstigationLogger(logger_name=custom_logger_name)
1116
assert instigation_logger.name == custom_logger_name
17+
18+
19+
class CrashyStartupComputeLogManager(NoOpComputeLogManager):
20+
@contextmanager
21+
def open_log_stream(self, log_key, io_type):
22+
raise Exception("OOPS")
23+
yield None
24+
25+
26+
class MockLogStreamComputeLogManager(NoOpComputeLogManager):
27+
@contextmanager
28+
def open_log_stream(self, log_key, io_type):
29+
yield mock.MagicMock()
30+
raise Exception("OOPS ON EXIT")
31+
32+
33+
def test_instigation_logger_start_failure(capsys):
34+
with instance_for_test(
35+
overrides={
36+
"compute_logs": {
37+
"module": "dagster_tests.storage_tests.test_instigation_logger",
38+
"class": "CrashyStartupComputeLogManager",
39+
}
40+
}
41+
) as instance:
42+
with InstigationLogger(log_key="foo", instance=instance) as logger:
43+
captured = capsys.readouterr()
44+
assert (
45+
captured.err.count("Exception initializing logger write stream: Exception: OOPS")
46+
== 1
47+
)
48+
logger.info("I can log without failing")
49+
50+
51+
def test_instigation_logger_log_failure(capsys):
52+
with instance_for_test(
53+
overrides={
54+
"compute_logs": {
55+
"module": "dagster_tests.storage_tests.test_instigation_logger",
56+
"class": "MockLogStreamComputeLogManager",
57+
}
58+
}
59+
) as instance:
60+
with InstigationLogger(log_key="foo", instance=instance) as logger:
61+
mock_write_stream = logger._capture_handler._write_stream # type: ignore # noqa
62+
mock_write_stream.write.side_effect = Exception("OOPS")
63+
64+
logger.info("HELLO")
65+
captured = capsys.readouterr()
66+
67+
assert (
68+
captured.err.count("Exception writing to logger event stream: Exception: OOPS") == 1
69+
)
70+
71+
captured = capsys.readouterr()
72+
73+
assert (
74+
captured.err.count("Exception closing logger write stream: Exception: OOPS ON EXIT")
75+
== 1
76+
)

0 commit comments

Comments
 (0)