log-foundry 0.10.2.dev31__tar.gz → 0.10.2.dev33__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/PKG-INFO +1 -1
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/worker.py +106 -13
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/LICENSE +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/README.md +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/__init__.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/util.py +0 -0
|
@@ -76,8 +76,10 @@ class Health(NamedTuple):
|
|
|
76
76
|
|
|
77
77
|
Attributes:
|
|
78
78
|
queued: Submissions currently buffered. Approximate by nature: it is read without
|
|
79
|
-
stopping the world, and
|
|
80
|
-
|
|
79
|
+
stopping the world, and counts the internal flush/shutdown markers alongside real
|
|
80
|
+
submissions — normally only in passing, but a ``flush()`` marker stranded by racing
|
|
81
|
+
``shutdown()`` is answered and then counted for the life of the process, since
|
|
82
|
+
``Queue`` offers no way to remove one specific item.
|
|
81
83
|
dropped: Submissions discarded because the queue was full (backpressure).
|
|
82
84
|
failed_batches: Batches abandoned after the retry budget was spent.
|
|
83
85
|
stopped_reason: The exception type name that terminated the drain thread, or ``None`` if
|
|
@@ -227,6 +229,7 @@ class Worker:
|
|
|
227
229
|
self._closers: list[threading.Thread] = []
|
|
228
230
|
self._queue: queue.Queue[object] = queue.Queue(maxsize=max_queue)
|
|
229
231
|
self._stop = threading.Event()
|
|
232
|
+
self._drain_finished = threading.Event()
|
|
230
233
|
self._shutdown_done = False
|
|
231
234
|
self._sink_closed = False
|
|
232
235
|
self._lock = threading.Lock()
|
|
@@ -338,8 +341,11 @@ class Worker:
|
|
|
338
341
|
"""Snapshots the delivery counters (SPEC-017 FR-005, SPEC-019 FR-003).
|
|
339
342
|
|
|
340
343
|
This stays valid after :meth:`shutdown`: the counters are plain integers that outlive
|
|
341
|
-
the thread, and the final drain consumes the queue
|
|
342
|
-
|
|
344
|
+
the thread, and the final drain consumes the queue. ``queued`` therefore reads 0 for a
|
|
345
|
+
worker nothing logged to afterwards — but not always: submissions accepted after the
|
|
346
|
+
shutdown stay queued on purpose (SPEC-030), and a ``flush()`` marker stranded by
|
|
347
|
+
racing it is answered and then counted, as ``Health.queued`` records. The same applies
|
|
348
|
+
to ``stopped_reason``, since a caller finding a dead
|
|
343
349
|
worker will usually call ``shutdown()`` next. Reading it after a shutdown is in fact
|
|
344
350
|
the point of ``retired`` and ``submitted_after_shutdown`` (SPEC-030 FR-001), which
|
|
345
351
|
report a state only a retired worker can be in.
|
|
@@ -408,6 +414,24 @@ class Worker:
|
|
|
408
414
|
queue ``put_nowait`` would skip the flush and return as though it had succeeded, the
|
|
409
415
|
one outcome a flush must never produce silently.
|
|
410
416
|
|
|
417
|
+
The drain's completion is re-checked **after** the put, and that second look is what
|
|
418
|
+
makes a ``timeout=None`` call safe. The checks above can both pass microseconds before
|
|
419
|
+
the drain finishes, leaving this marker queued behind something that will never read it
|
|
420
|
+
— a bounded caller then waits out its timeout, which SPEC-021 accepts as correct either
|
|
421
|
+
way, but an unbounded one waits forever.
|
|
422
|
+
|
|
423
|
+
It tests ``_drain_finished`` and not only ``is_alive()``, because the two are not the
|
|
424
|
+
same instant and the gap between them is where the hang survives: the terminal-failure
|
|
425
|
+
path sweeps for markers and *then* returns, so a marker queued after that sweep sits
|
|
426
|
+
behind a thread still reading as alive. The flag is set **before** the sweep, and a
|
|
427
|
+
``put`` and the sweep's snapshot both take the queue's own mutex, so a marker either
|
|
428
|
+
lands before the snapshot and is answered, or lands after it and finds the flag set.
|
|
429
|
+
|
|
430
|
+
Reporting is by the **marker**, never by the check alone. A drain that answered this
|
|
431
|
+
marker and then exited has delivered, and saying otherwise would be a false failure —
|
|
432
|
+
one ``swap_sink`` reads as an unconfirmed drain, counting ``incomplete_swaps``, leaving
|
|
433
|
+
the previous sink open and writing a loss line for a swap that in fact completed.
|
|
434
|
+
|
|
411
435
|
Args:
|
|
412
436
|
timeout: Seconds bounding the whole call — one deadline shared by the put and the
|
|
413
437
|
wait, so the two cannot add up to twice the timeout. ``None`` waits indefinitely.
|
|
@@ -433,6 +457,8 @@ class Worker:
|
|
|
433
457
|
self._queue.put(marker, timeout=timeout)
|
|
434
458
|
except queue.Full:
|
|
435
459
|
return False
|
|
460
|
+
if self._drain_finished.is_set() or not self._thread.is_alive():
|
|
461
|
+
return marker.event.is_set() and marker.delivered
|
|
436
462
|
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
437
463
|
if not marker.event.wait(remaining):
|
|
438
464
|
return False
|
|
@@ -638,6 +664,36 @@ class Worker:
|
|
|
638
664
|
``close()`` twice on a sink that may have partially released its resources; what
|
|
639
665
|
SPEC-025 FR-004 changed is that the failure is announced rather than swallowed.
|
|
640
666
|
|
|
667
|
+
**The sentinel is queued before ``_stop`` is set, and while the drain loop is running
|
|
668
|
+
that order is what makes it impossible to strand.** Both ways of leaving the loop —
|
|
669
|
+
taking the sentinel, or seeing ``_stop`` — can only happen once it is already in the
|
|
670
|
+
queue, so either that ``get`` consumes it or :meth:`_final_drain` does. The reverse
|
|
671
|
+
order left a window in which the loop read ``_stop``, exited, and finished its final
|
|
672
|
+
drain before the sentinel landed. It never lost an event, but left ``health().queued``
|
|
673
|
+
reading 1 for the life of the process. The rate is load-dependent and not worth
|
|
674
|
+
quoting as a property — rare when idle, and repeatedly reproduced between roughly one
|
|
675
|
+
shutdown in 14 and one in 50 with spinner threads and a tightened switch interval.
|
|
676
|
+
|
|
677
|
+
Paying for that order needs :meth:`_drain` to break on the sentinel rather than loop, or
|
|
678
|
+
a thread taking it before ``_stop`` was set would block for another ``flush_interval``
|
|
679
|
+
— measured stalling low single-digit percentages of shutdowns under load for the entire
|
|
680
|
+
budget, latching a ``stopped_reason`` of ``"ShutdownTimeout"``, which is far worse than
|
|
681
|
+
the cosmetic problem being fixed.
|
|
682
|
+
|
|
683
|
+
The premise is the loop, so the put is skipped once the drain has stopped reading. A
|
|
684
|
+
drain that died terminally (SPEC-019) is not coming back for a wake-up, and queueing one
|
|
685
|
+
for it would strand it permanently — reintroducing the symptom on the one path the
|
|
686
|
+
ordering cannot reach. The gate is ``_drain_finished`` rather than ``is_alive()``,
|
|
687
|
+
because they are not the same instant: the thread is still alive throughout
|
|
688
|
+
:meth:`_terminal_failure`, which writes to stderr and can block on a slow reader, and a
|
|
689
|
+
liveness test would queue a sentinel through that whole window. The flag is set before
|
|
690
|
+
that call, so this one does not.
|
|
691
|
+
|
|
692
|
+
:meth:`_release_waiters` runs on the way out for the sibling case the ordering cannot
|
|
693
|
+
reach: a ``flush()`` that passed its liveness check microseconds before the thread
|
|
694
|
+
finished can still queue a marker nothing will answer, and with ``timeout=None`` that
|
|
695
|
+
caller waits forever rather than merely too long.
|
|
696
|
+
|
|
641
697
|
The worker does not come back, and :meth:`submit` keeps accepting afterwards — so a
|
|
642
698
|
caller that logs again queues events nothing will drain. That is reported rather than
|
|
643
699
|
prevented, through ``retired`` and ``submitted_after_shutdown`` (SPEC-030 FR-001) and
|
|
@@ -664,11 +720,12 @@ class Worker:
|
|
|
664
720
|
self._close_if_owed()
|
|
665
721
|
self._join_closers(None if deadline is None else max(0.0, deadline - time.monotonic()))
|
|
666
722
|
return
|
|
723
|
+
if not self._drain_finished.is_set():
|
|
724
|
+
try:
|
|
725
|
+
self._queue.put_nowait(_SHUTDOWN)
|
|
726
|
+
except queue.Full:
|
|
727
|
+
pass
|
|
667
728
|
self._stop.set()
|
|
668
|
-
try:
|
|
669
|
-
self._queue.put_nowait(_SHUTDOWN)
|
|
670
|
-
except queue.Full:
|
|
671
|
-
pass
|
|
672
729
|
self._thread.join(timeout)
|
|
673
730
|
if self._thread.is_alive():
|
|
674
731
|
queued = self._queued_or_unknown()
|
|
@@ -682,6 +739,7 @@ class Worker:
|
|
|
682
739
|
f"because the worker thread is still using it",
|
|
683
740
|
)
|
|
684
741
|
return
|
|
742
|
+
self._release_waiters()
|
|
685
743
|
self._close_if_owed()
|
|
686
744
|
self._join_closers(None if deadline is None else max(0.0, deadline - time.monotonic()))
|
|
687
745
|
|
|
@@ -826,6 +884,18 @@ class Worker:
|
|
|
826
884
|
``BaseException``. It records and exits; looping onward past a ``KeyboardInterrupt``
|
|
827
885
|
would be a worse failure than the one this prevents.
|
|
828
886
|
|
|
887
|
+
The two ``finally`` blocks are nested rather than merged, and the order they impose is
|
|
888
|
+
load-bearing twice over. ``_drain_finished`` is set the instant the loop stops reading
|
|
889
|
+
the queue — *before* :meth:`_terminal_failure`, which writes to stderr and can block on
|
|
890
|
+
a slow reader — so a ``shutdown()`` arriving during that window sees a drain that is
|
|
891
|
+
already finished and declines to queue a sentinel nothing would consume. And it is set
|
|
892
|
+
before :meth:`_release_waiters`, so a marker either lands ahead of that sweep's snapshot
|
|
893
|
+
and is answered by it, or lands behind it and finds the flag set; both take the queue's
|
|
894
|
+
own mutex, which is what leaves no gap between the two. Sweeping here rather than only
|
|
895
|
+
in :meth:`Worker.shutdown` is what covers the paths ``shutdown`` never reaches — a
|
|
896
|
+
terminal failure, and a bounded shutdown that expired while this thread was still
|
|
897
|
+
inside an emit.
|
|
898
|
+
|
|
829
899
|
Args:
|
|
830
900
|
None.
|
|
831
901
|
|
|
@@ -837,9 +907,13 @@ class Worker:
|
|
|
837
907
|
"""
|
|
838
908
|
pending: list[list[dict[str, object]]] = []
|
|
839
909
|
try:
|
|
840
|
-
|
|
910
|
+
try:
|
|
911
|
+
self._drain(pending)
|
|
912
|
+
finally:
|
|
913
|
+
self._drain_finished.set()
|
|
841
914
|
except BaseException as exc:
|
|
842
915
|
self._terminal_failure(exc, len(pending))
|
|
916
|
+
finally:
|
|
843
917
|
self._release_waiters()
|
|
844
918
|
|
|
845
919
|
def _release_waiters(self) -> None:
|
|
@@ -852,10 +926,20 @@ class Worker:
|
|
|
852
926
|
``health().queued`` and the terminal line report; each keeps its pessimistic
|
|
853
927
|
``delivered``, which is the truth here.
|
|
854
928
|
|
|
929
|
+
It is called from two places. The terminal-failure path is the original one. The clean
|
|
930
|
+
:meth:`shutdown` path was added because that same enqueue-after-the-drain race happens
|
|
931
|
+
there too, and hurts more: measured stranding a marker in 13 of 400 shutdowns raced
|
|
932
|
+
against a ``flush()`` under load, where the caller sat out its whole timeout — and
|
|
933
|
+
``flush(timeout=None)``, which the API documents as supported, waits forever rather
|
|
934
|
+
than too long. The marker keeps its pessimistic ``delivered``, which is the honest
|
|
935
|
+
answer: the drain that would have carried it is gone.
|
|
936
|
+
|
|
855
937
|
One residual race, stated rather than papered over: a ``flush()`` that passed its
|
|
856
|
-
liveness check microseconds before
|
|
857
|
-
|
|
858
|
-
|
|
938
|
+
liveness check microseconds before *this* sweep can still enqueue a marker after it,
|
|
939
|
+
and that one waits out its timeout — then returns False, which is correct either way.
|
|
940
|
+
A marker left queued is also still counted by ``health().queued``, which describes
|
|
941
|
+
submissions; removing it would mean deleting a specific item, which ``Queue`` has no
|
|
942
|
+
public way to do, and the read above is the access ``architecture.md`` §13 sanctions.
|
|
859
943
|
|
|
860
944
|
The reliance on ``queue.Queue``'s private ``mutex`` and ``queue`` is deliberate and is
|
|
861
945
|
recorded in ``architecture.md`` §13 Known Constraints (SPEC-031 FR-005): there is no
|
|
@@ -928,6 +1012,13 @@ class Worker:
|
|
|
928
1012
|
advanced even when idle: otherwise the timeout collapses to zero and ``get`` busy-spins
|
|
929
1013
|
a core.
|
|
930
1014
|
|
|
1015
|
+
The shutdown sentinel **breaks** rather than falling through to the loop condition.
|
|
1016
|
+
:meth:`shutdown` queues it before setting ``_stop``, so a thread that takes it may find
|
|
1017
|
+
``_stop`` still clear; continuing would re-enter ``get`` and block for another
|
|
1018
|
+
``flush_interval``, which is a slow shutdown rather than a prompt one. Leaving
|
|
1019
|
+
immediately is safe because the only thing after the loop is :meth:`_final_drain`, which
|
|
1020
|
+
collects whatever is still queued — the sentinel is a wake-up, never a fence.
|
|
1021
|
+
|
|
931
1022
|
Args:
|
|
932
1023
|
pending: The accumulator owned by :meth:`_run`, which reports its size on a terminal
|
|
933
1024
|
failure. It is mutated in place rather than rebound, so that count is accurate.
|
|
@@ -953,7 +1044,9 @@ class Worker:
|
|
|
953
1044
|
last_flush = time.monotonic()
|
|
954
1045
|
item.event.set()
|
|
955
1046
|
continue
|
|
956
|
-
if item is
|
|
1047
|
+
if item is _SHUTDOWN:
|
|
1048
|
+
break
|
|
1049
|
+
if item is not None:
|
|
957
1050
|
pending.append(cast("list[dict[str, object]]", item))
|
|
958
1051
|
now = time.monotonic()
|
|
959
1052
|
if len(pending) >= self.batch_size or now - last_flush >= self.flush_interval:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|