Skip to content

refactor(observability): run every periodic flush on one background loop with one shutdown bound [FND-4648] - #4819

Merged
cmgrote merged 3 commits into
mainfrom
christophergrote/fnd-4648
Nov 9, 2026
Merged

cmgrote merged 3 commits into
mainfrom
christophergrote/fnd-4648

Conversation

@cmgrote

@cmgrote cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Changelog

  • New application_sdk/observability/background_loop.py: one daemon thread (atlan-observability-loop) running one event loop, started on first use, fork-aware.
  • Logs, metrics, traces and sizing all start their periodic flush through AtlanObservability._start_periodic_flush(), always on that loop. This replaces three start styles: a task on whatever loop was running, an asyncio.run thread, and a new_event_loop + run_forever thread (plus sizing's hand-built loop on a thread).
  • SegmentClient runs its queue worker on the shared loop instead of its own thread and loop. Its queue and HTTP client are built at start, so the up-to-5 s init wait and the per-send 1 s init wait are gone. When the metrics flush, which now runs on the same loop, calls flush(), it awaits _flush_queue directly instead of bridging to itself.
  • One atexit shutdown, bounded by the new ATLAN_OBSERVABILITY_SHUTDOWN_TIMEOUT_SECONDS (default 10 s), stops everything: each started task is cancelled once and makes its final flush or sends its last batch. Other tasks on the loop (batch flushes add_record started there) are awaited, not cancelled, because cancelling one deletes its half-written file. Then the loop stops.

Behaviour changes for reviewers

  • Metrics and traces flushes built inside a running loop no longer run on that loop (e.g. the worker's). They run on the observability loop, so they no longer share the event loop with activity heartbeats.
  • The metrics adapter no longer registers atexit(segment_client.close). The loop shutdown sends the Segment worker's last batch. The old close waited up to 10 s for the Segment thread; the new single bound is also 10 s, so worst-case exit time does not grow.
  • A cancel that arrives before a task's first step is deferred until the task has started. Otherwise the coroutine never enters its try and its final flush never runs. Found while writing the tests; both race tests went red before the fix.
  • Not changed on purpose: the console writer stays a plain thread, because its job is a blocking write() to stdout and on the shared loop a stalled pipe would stall every flush. dapr_log_forwarder is a separate process. _sync_flush's per-error temporary loop is not a periodic flush. The OTLP and console atexit drains keep their own bounds, so the shutdown-budget work can unify them later.
  • Removed private seams: AtlanLoggerAdapter._spawn_flush_thread / _start_asyncio_flush, AtlanMetricsAdapter._spawn_flush_thread / _start_asyncio_flush, AtlanTracesAdapter._start_asyncio_flush, SizingObservabilitySink._flush_thread / _flush_forever, SegmentClient._start_worker_thread* / _worker_thread / _initialized_event / _worker_task. All are underscore-private.

Additional context

  • Linear: FND-4648 (plan recorded as a comment there).
  • New tests/unit/observability/test_background_loop.py: one thread for all tasks, cancel-once final flush, await-don't-cancel, bounded shutdown, nothing starts after shutdown, cancel racing the start, and two real AtlanObservability sinks whose final flushes both land on one shutdown. Mutation check: making the drain cancel every task turns test_awaits_other_tasks_instead_of_cancelling_them red.
  • Existing tests that pinned the removed thread seams now assert the start goes through the shared loop.
  • Every changed file is inside the planned list for this issue.
  • Security-relevant: none. No auth, network destination or credential handling changed.

Checklist

  • Additional tests added
  • All CI checks passed
  • Relevant documentation updated

Copyleft License Compliance

  • Have you used any code that is subject to a Copyleft license (e.g., GPL, AGPL, LGPL)?
  • If yes, have you modified the code in the context of this project? please share additional details.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbKqGbSfZqfoW5hkNx3D61

…oop with one shutdown bound [FND-4648]

Logs, metrics, traces and sizing each started their periodic flush their
own way (a task on the running loop, an asyncio.run thread, a
new_event_loop + run_forever thread, a hand-built loop on a thread), and
the Segment client ran a fourth thread with a private loop and a 5 s init
wait. Each had its own shutdown, or none.

Add observability/background_loop.py: one daemon thread
(atlan-observability-loop) with one event loop, started on first use.
Every sink starts its flush through AtlanObservability._start_periodic_flush()
on that loop, and the Segment sender runs there as a task. One atexit
shutdown, bounded by ATLAN_OBSERVABILITY_SHUTDOWN_TIMEOUT_SECONDS
(default 10 s), cancels each started task once so it makes its final
flush, awaits other tasks on the loop without cancelling them, and stops
the loop. A cancel that lands before a task's first step is deferred so
the final flush still runs.

The console writer stays a plain thread: it does blocking stdout writes
that must not stall the flushes.

Claude-Session: https://claude.ai/code/session_01AbKqGbSfZqfoW5hkNx3D61
@linear

linear Bot commented Nov 9, 2026

Copy link
Copy Markdown

FND-4648

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

📜 Docstring Coverage Report

RESULT: PASSED (minimum: 75.0%, actual: 80.0%)

Detailed Coverage Report
= Coverage for /home/runner/work/application-sdk/application-sdk/application_sdk/ =
----------------------------------- Summary ------------------------------------
| Name                                                           | Total | Miss | Cover | Cover% |
|----------------------------------------------------------------|-------|------|-------|--------|
| __init__.py                                                    |     1 |    0 |     1 |   100% |
| _build_info.py                                                 |     2 |    0 |     2 |   100% |
| _context_errors.py                                             |     7 |    3 |     4 |    57% |
| _discovery_errors.py                                           |     7 |    0 |     7 |   100% |
| _install.py                                                    |     6 |    0 |     6 |   100% |
| _logging.py                                                    |    13 |    8 |     5 |    38% |
| constants.py                                                   |    14 |   10 |     4 |    29% |
| discovery.py                                                   |    12 |    3 |     9 |    75% |
| main.py                                                        |    55 |   17 |    38 |    69% |
| main_errors.py                                                 |     5 |    0 |     5 |   100% |
| pkl_version.py                                                 |     1 |    0 |     1 |   100% |
| version.py                                                     |     1 |    0 |     1 |   100% |
| _runtime/__init__.py                                           |     1 |    0 |     1 |   100% |
| _runtime/cgroup.py                                             |     6 |    2 |     4 |    67% |
| _runtime/offload.py                                            |    65 |   17 |    48 |    74% |
| _runtime/progress.py                                           |    32 |    2 |    30 |    94% |
| app/__init__.py                                                |     1 |    0 |     1 |   100% |
| app/_artifact_schema_guard.py                                  |     8 |    0 |     8 |   100% |
| app/_ep_registration.py                                        |     6 |    0 |     6 |   100% |
| app/_generated_tree.py                                         |     1 |    0 |     1 |   100% |
| app/base.py                                                    |   104 |   23 |    81 |    78% |
| app/base_errors.py                                             |     5 |    0 |     5 |   100% |
| app/build_identity.py                                          |     1 |    0 |     1 |   100% |
| app/client.py                                                  |     1 |    0 |     1 |   100% |
| app/context.py                                                 |    42 |    2 |    40 |    95% |
| app/entrypoint.py                                              |    20 |    5 |    15 |    75% |
| app/registry.py                                                |    40 |   11 |    29 |    72% |
| app/task.py                                                    |    17 |    6 |    11 |    65% |
| clients/__init__.py                                            |     2 |    1 |     1 |    50% |
| clients/_interface.py                                          |     6 |    1 |     5 |    83% |
| clients/_sql_failure_classifier.py                             |     5 |    0 |     5 |   100% |
| clients/base.py                                                |    22 |   10 |    12 |    55% |
| clients/http_errors.py                                         |     2 |    0 |     2 |   100% |
| clients/models.py                                              |     2 |    0 |     2 |   100% |
| clients/redis.py                                               |    27 |    0 |    27 |   100% |
| clients/redis_errors.py                                        |     5 |    0 |     5 |   100% |
| clients/sql.py                                                 |    64 |   22 |    42 |    66% |
| clients/sql_errors.py                                          |    15 |    0 |    15 |   100% |
| clients/sql_typecasters.py                                     |    14 |    4 |    10 |    71% |
| clients/ssl_utils.py                                           |     8 |    0 |     8 |   100% |
| clients/azure/__init__.py                                      |     1 |    0 |     1 |   100% |
| clients/azure/auth.py                                          |     7 |    0 |     7 |   100% |
| clients/azure/azure_errors.py                                  |     8 |    0 |     8 |   100% |
| clients/azure/client.py                                        |    14 |    0 |    14 |   100% |
| common/__init__.py                                             |     1 |    0 |     1 |   100% |
| common/_env.py                                                 |     2 |    0 |     2 |   100% |
| common/_generated_tree.py                                      |     9 |    0 |     9 |   100% |
| common/_listing.py                                             |     7 |    0 |     7 |   100% |
| common/asset_serialization.py                                  |    20 |    5 |    15 |    75% |
| common/atomic.py                                               |    17 |    0 |    17 |   100% |
| common/aws_utils.py                                            |    13 |    1 |    12 |    92% |
| common/aws_utils_errors.py                                     |     8 |    0 |     8 |   100% |
| common/backoff.py                                              |     9 |    1 |     8 |    89% |
| common/build_identity.py                                       |     2 |    0 |     2 |   100% |
| common/concurrency.py                                          |     3 |    0 |     3 |   100% |
| common/dispatch.py                                             |     7 |    2 |     5 |    71% |
| common/entity_envelope.py                                      |    10 |    0 |    10 |   100% |
| common/env_warnings.py                                         |     2 |    0 |     2 |   100% |
| common/error_codes.py                                          |    15 |    3 |    12 |    80% |
| common/errors.py                                               |     8 |    0 |     8 |   100% |
| common/file_converter.py                                       |     9 |    5 |     4 |    44% |
| common/file_ops.py                                             |    16 |    1 |    15 |    94% |
| common/filter_matching.py                                      |     9 |    3 |     6 |    67% |
| common/last_sync.py                                            |     6 |    0 |     6 |   100% |
| common/logger.py                                               |     1 |    0 |     1 |   100% |
| common/models.py                                               |     4 |    2 |     2 |    50% |
| common/path.py                                                 |     2 |    1 |     1 |    50% |
| common/restart_marker.py                                       |     8 |    0 |     8 |   100% |
| common/spillable_dict.py                                       |    17 |   11 |     6 |    35% |
| common/sql_filters.py                                          |    18 |    4 |    14 |    78% |
| common/sql_filters_errors.py                                   |     2 |    0 |     2 |   100% |
| common/task_queue.py                                           |    10 |    0 |    10 |   100% |
| common/transforms.py                                           |     5 |    0 |     5 |   100% |
| common/types.py                                                |     2 |    0 |     2 |   100% |
| common/utils.py                                                |     2 |    0 |     2 |   100% |
| common/incremental/__init__.py                                 |     3 |    0 |     3 |   100% |
| common/incremental/helpers.py                                  |    13 |    0 |    13 |   100% |
| common/incremental/incremental_errors.py                       |    12 |    0 |    12 |   100% |
| common/incremental/marker.py                                   |    13 |    0 |    13 |   100% |
| common/incremental/models.py                                   |    11 |    0 |    11 |   100% |
| common/incremental/column_extraction/__init__.py               |     1 |    0 |     1 |   100% |
| common/incremental/column_extraction/analysis.py               |     4 |    0 |     4 |   100% |
| common/incremental/column_extraction/backfill.py               |     3 |    0 |     3 |   100% |
| common/incremental/state/__init__.py                           |     1 |    1 |     0 |     0% |
| common/incremental/state/incremental_diff.py                   |     8 |    0 |     8 |   100% |
| common/incremental/state/state_reader.py                       |     7 |    0 |     7 |   100% |
| common/incremental/state/state_writer.py                       |    15 |    1 |    14 |    93% |
| common/incremental/state/store.py                              |    23 |    6 |    17 |    74% |
| common/incremental/state/table_scope.py                        |     8 |    0 |     8 |   100% |
| common/incremental/storage/__init__.py                         |     1 |    1 |     0 |     0% |
| common/incremental/storage/duckdb_utils.py                     |    24 |    8 |    16 |    67% |
| common/incremental/storage/rocksdb_utils.py                    |    11 |    5 |     6 |    55% |
| contracts/__init__.py                                          |     3 |    2 |     1 |    33% |
| contracts/base.py                                              |    39 |    7 |    32 |    82% |
| contracts/cleanup.py                                           |     5 |    0 |     5 |   100% |
| contracts/compat.py                                            |     9 |    1 |     8 |    89% |
| contracts/events.py                                            |    12 |    0 |    12 |   100% |
| contracts/storage.py                                           |    12 |    2 |    10 |    83% |
| contracts/types.py                                             |    24 |    1 |    23 |    96% |
| contracts/types_errors.py                                      |     2 |    0 |     2 |   100% |
| credentials/__init__.py                                        |     3 |    2 |     1 |    33% |
| credentials/agent.py                                           |    21 |    4 |    17 |    81% |
| credentials/atlan.py                                           |    12 |    6 |     6 |    50% |
| credentials/atlan_client.py                                    |     6 |    0 |     6 |   100% |
| credentials/errors.py                                          |    20 |   12 |     8 |    40% |
| credentials/extra.py                                           |     3 |    1 |     2 |    67% |
| credentials/git.py                                             |     9 |    6 |     3 |    33% |
| credentials/ingress.py                                         |     9 |    0 |     9 |   100% |
| credentials/oauth.py                                           |    13 |    2 |    11 |    85% |
| credentials/ref.py                                             |    17 |    1 |    16 |    94% |
| credentials/registry.py                                        |    11 |    3 |     8 |    73% |
| credentials/requirements.py                                    |    19 |    7 |    12 |    63% |
| credentials/resolver.py                                        |    11 |    4 |     7 |    64% |
| credentials/routing.py                                         |    10 |    1 |     9 |    90% |
| credentials/spec.py                                            |    23 |   11 |    12 |    52% |
| credentials/types.py                                           |    35 |   17 |    18 |    51% |
| credentials/utils.py                                           |    16 |    3 |    13 |    81% |
| dev/__init__.py                                                |     1 |    0 |     1 |   100% |
| dev/_dapr.py                                                   |    17 |    3 |    14 |    82% |
| dev/_dapr_errors.py                                            |    10 |    9 |     1 |    10% |
| dev/_embedded.py                                               |     4 |    0 |     4 |   100% |
| dev/_pkl_errors.py                                             |     3 |    0 |     3 |   100% |
| dev/pkl.py                                                     |    16 |    6 |    10 |    62% |
| errors/__init__.py                                             |     4 |    1 |     3 |    75% |
| errors/base.py                                                 |    15 |    2 |    13 |    87% |
| errors/categories.py                                           |     3 |    0 |     3 |   100% |
| errors/http.py                                                 |     4 |    1 |     3 |    75% |
| errors/leaves.py                                               |    26 |    8 |    18 |    69% |
| errors/wire.py                                                 |     5 |    1 |     4 |    80% |
| execution/__init__.py                                          |     1 |    0 |     1 |   100% |
| execution/decorators.py                                        |     3 |    2 |     1 |    33% |
| execution/errors.py                                            |     2 |    0 |     2 |   100% |
| execution/heartbeat.py                                         |    26 |    4 |    22 |    85% |
| execution/progress.py                                          |     6 |    0 |     6 |   100% |
| execution/progress_telemetry.py                                |     7 |    1 |     6 |    86% |
| execution/retry.py                                             |     9 |    0 |     9 |   100% |
| execution/run_length.py                                        |     7 |    1 |     6 |    86% |
| execution/sandbox.py                                           |     4 |    0 |     4 |   100% |
| execution/settings.py                                          |     9 |    2 |     7 |    78% |
| execution/shutdown.py                                          |     4 |    0 |     4 |   100% |
| execution/_temporal/__init__.py                                |     1 |    1 |     0 |     0% |
| execution/_temporal/_activity_errors.py                        |     9 |    0 |     9 |   100% |
| execution/_temporal/_backend_errors.py                         |     6 |    4 |     2 |    33% |
| execution/_temporal/_lock_errors.py                            |     5 |    0 |     5 |   100% |
| execution/_temporal/_store_records.py                          |    17 |    3 |    14 |    82% |
| execution/_temporal/activities.py                              |    12 |    0 |    12 |   100% |
| execution/_temporal/activity_utils.py                          |     7 |    0 |     7 |   100% |
| execution/_temporal/auth.py                                    |    16 |    0 |    16 |   100% |
| execution/_temporal/backend.py                                 |    18 |    3 |    15 |    83% |
| execution/_temporal/converter.py                               |     3 |    0 |     3 |   100% |
| execution/_temporal/credential_sweep.py                        |     5 |    0 |     5 |   100% |
| execution/_temporal/eviction_retry.py                          |     4 |    0 |     4 |   100% |
| execution/_temporal/lock_activities.py                         |     5 |    0 |     5 |   100% |
| execution/_temporal/poll_state.py                              |     7 |    1 |     6 |    86% |
| execution/_temporal/preflight_gate.py                          |    93 |    8 |    85 |    91% |
| execution/_temporal/preflight_persist.py                       |    15 |    0 |    15 |   100% |
| execution/_temporal/preflight_transport.py                     |     8 |    3 |     5 |    62% |
| execution/_temporal/sdr.py                                     |    19 |    8 |    11 |    58% |
| execution/_temporal/store_assert.py                            |    25 |    4 |    21 |    84% |
| execution/_temporal/worker.py                                  |    19 |    6 |    13 |    68% |
| execution/_temporal/workflows.py                               |     3 |    0 |     3 |   100% |
| execution/_temporal/interceptors/__init__.py                   |     1 |    0 |     1 |   100% |
| execution/_temporal/interceptors/events.py                     |    28 |    3 |    25 |    89% |
| execution/_temporal/interceptors/liveness.py                   |    11 |    9 |     2 |    18% |
| execution/_temporal/interceptors/lock.py                       |    10 |    2 |     8 |    80% |
| execution/_temporal/interceptors/log.py                        |    25 |   12 |    13 |    52% |
| execution/_temporal/interceptors/metrics.py                    |    18 |   15 |     3 |    17% |
| execution/_temporal/interceptors/outputs.py                    |    22 |    9 |    13 |    59% |
| execution/_temporal/interceptors/sizing.py                     |    18 |    9 |     9 |    50% |
| execution/_temporal/interceptors/trace.py                      |     6 |    4 |     2 |    33% |
| handler/__init__.py                                            |     3 |    2 |     1 |    33% |
| handler/_dispatch.py                                           |    11 |    5 |     6 |    55% |
| handler/_preflight_outcome.py                                  |    16 |    0 |    16 |   100% |
| handler/_warmup.py                                             |    10 |    0 |    10 |   100% |
| handler/asgi.py                                                |    13 |    4 |     9 |    69% |
| handler/base.py                                                |    17 |    3 |    14 |    82% |
| handler/context.py                                             |    24 |    7 |    17 |    71% |
| handler/contracts.py                                           |    56 |   11 |    45 |    80% |
| handler/manifest.py                                            |     5 |    0 |     5 |   100% |
| handler/metadata_routes.py                                     |    29 |    5 |    24 |    83% |
| handler/object_routes.py                                       |    28 |   12 |    16 |    57% |
| handler/request_contract.py                                    |     5 |    1 |     4 |    80% |
| handler/routes.py                                              |    33 |   11 |    22 |    67% |
| handler/service.py                                             |    49 |   19 |    30 |    61% |
| handler/service_errors.py                                      |     4 |    0 |     4 |   100% |
| handler/source_probe_errors.py                                 |    13 |    0 |    13 |   100% |
| handler/source_probes.py                                       |    28 |   14 |    14 |    50% |
| infrastructure/__init__.py                                     |     1 |    0 |     1 |   100% |
| infrastructure/_secret_utils.py                                |     2 |    0 |     2 |   100% |
| infrastructure/_single_key_probe.py                            |     7 |    2 |     5 |    71% |
| infrastructure/bindings.py                                     |    16 |    3 |    13 |    81% |
| infrastructure/capacity.py                                     |    11 |    0 |    11 |   100% |
| infrastructure/context.py                                      |     8 |    0 |     8 |   100% |
| infrastructure/credential_vault.py                             |     8 |    3 |     5 |    62% |
| infrastructure/pubsub.py                                       |    13 |    3 |    10 |    77% |
| infrastructure/secrets.py                                      |    30 |    9 |    21 |    70% |
| infrastructure/state.py                                        |    10 |    7 |     3 |    30% |
| infrastructure/_dapr/__init__.py                               |     1 |    0 |     1 |   100% |
| infrastructure/_dapr/_dapr_errors.py                           |     3 |    0 |     3 |   100% |
| infrastructure/_dapr/client.py                                 |    36 |    4 |    32 |    89% |
| infrastructure/_dapr/credential_vault.py                       |    19 |    6 |    13 |    68% |
| infrastructure/_dapr/http.py                                   |    30 |   20 |    10 |    33% |
| infrastructure/_redis/__init__.py                              |     1 |    0 |     1 |   100% |
| infrastructure/_redis/capacity.py                              |     9 |    4 |     5 |    56% |
| observability/__init__.py                                      |     1 |    1 |     0 |     0% |
| observability/_objectstore_metric_exporter.py                  |    14 |    8 |     6 |    43% |
| observability/_objectstore_metric_reader.py                    |     2 |    0 |     2 |   100% |
| observability/_prometheus_enrichment.py                        |     6 |    3 |     3 |    50% |
| observability/background_loop.py                               |    26 |    6 |    20 |    77% |
| observability/cgroup.py                                        |    20 |    1 |    19 |    95% |
| observability/console_sink.py                                  |    15 |    6 |     9 |    60% |
| observability/context.py                                       |     6 |    0 |     6 |   100% |
| observability/correlation.py                                   |     6 |    0 |     6 |   100% |
| observability/dapr_log_forwarder.py                            |    17 |    4 |    13 |    76% |
| observability/events.py                                        |     1 |    0 |     1 |   100% |
| observability/logger_adaptor.py                                |    61 |   12 |    49 |    80% |
| observability/logger_adaptor_errors.py                         |     2 |    0 |     2 |   100% |
| observability/metrics.py                                       |     8 |    6 |     2 |    25% |
| observability/metrics_adaptor.py                               |    12 |    2 |    10 |    83% |
| observability/models.py                                        |     6 |    0 |     6 |   100% |
| observability/observability.py                                 |    27 |    5 |    22 |    81% |
| observability/pushgateway.py                                   |    16 |   11 |     5 |    31% |
| observability/pushgateway_errors.py                            |     3 |    0 |     3 |   100% |
| observability/resource_sampler.py                              |     6 |    0 |     6 |   100% |
| observability/segment_client.py                                |    21 |    3 |    18 |    86% |
| observability/sizing.py                                        |    16 |    7 |     9 |    56% |
| observability/sizing_census.py                                 |     8 |    3 |     5 |    62% |
| observability/sizing_inputs.py                                 |    13 |    3 |    10 |    77% |
| observability/sizing_sink.py                                   |    12 |    1 |    11 |    92% |
| observability/temporal_core_metrics.py                         |    11 |    2 |     9 |    82% |
| observability/trace_context.py                                 |     2 |    0 |     2 |   100% |
| observability/traces_adaptor.py                                |    14 |    1 |    13 |    93% |
| observability/utils.py                                         |     8 |    1 |     7 |    88% |
| observability/decorators/observability_decorator.py            |     7 |    4 |     3 |    43% |
| outputs/__init__.py                                            |     2 |    0 |     2 |   100% |
| outputs/collector.py                                           |    10 |    0 |    10 |   100% |
| outputs/models.py                                              |     3 |    0 |     3 |   100% |
| server/__init__.py                                             |     1 |    0 |     1 |   100% |
| server/health.py                                               |    24 |    0 |    24 |   100% |
| server/fastapi/models.py                                       |    21 |   17 |     4 |    19% |
| server/fastapi/utils.py                                        |     5 |    0 |     5 |   100% |
| server/mcp/__init__.py                                         |     2 |    2 |     0 |     0% |
| server/mcp/decorators.py                                       |     3 |    1 |     2 |    67% |
| server/mcp/models.py                                           |     2 |    2 |     0 |     0% |
| server/mcp/server.py                                           |     7 |    1 |     6 |    86% |
| server/middleware/__init__.py                                  |     1 |    0 |     1 |   100% |
| server/middleware/_constants.py                                |     1 |    0 |     1 |   100% |
| server/middleware/log.py                                       |     4 |    3 |     1 |    25% |
| storage/__init__.py                                            |     1 |    0 |     1 |   100% |
| storage/_budget.py                                             |    11 |    5 |     6 |    55% |
| storage/_concurrency.py                                        |     7 |    3 |     4 |    57% |
| storage/_credential_providers.py                               |     6 |    0 |     6 |   100% |
| storage/_local_marker.py                                       |    13 |    1 |    12 |    92% |
| storage/_locks.py                                              |     5 |    1 |     4 |    80% |
| storage/_manifest.py                                           |     3 |    0 |     3 |   100% |
| storage/_obstore_config.py                                     |    13 |    0 |    13 |   100% |
| storage/_telemetry.py                                          |     5 |    0 |     5 |   100% |
| storage/batch.py                                               |    31 |    5 |    26 |    84% |
| storage/binding.py                                             |    29 |    1 |    28 |    97% |
| storage/chunked.py                                             |    20 |    2 |    18 |    90% |
| storage/cloud.py                                               |    30 |    6 |    24 |    80% |
| storage/errors.py                                              |    61 |   38 |    23 |    38% |
| storage/factory.py                                             |     3 |    0 |     3 |   100% |
| storage/file_ref_sync.py                                       |    19 |    3 |    16 |    84% |
| storage/integrity.py                                           |    14 |    0 |    14 |   100% |
| storage/ops.py                                                 |    47 |    6 |    41 |    87% |
| storage/preflight.py                                           |    13 |    0 |    13 |   100% |
| storage/reference.py                                           |    34 |    3 |    31 |    91% |
| storage/rolling.py                                             |    39 |   15 |    24 |    62% |
| storage/rolling_errors.py                                      |     4 |    0 |     4 |   100% |
| storage/transfer.py                                            |    14 |    0 |    14 |   100% |
| storage/formats/__init__.py                                    |    33 |    0 |    33 |   100% |
| storage/formats/format_errors.py                               |    16 |    0 |    16 |   100% |
| storage/formats/json.py                                        |    16 |    6 |    10 |    62% |
| storage/formats/parquet.py                                     |    33 |    5 |    28 |    85% |
| storage/formats/utils.py                                       |    11 |    2 |     9 |    82% |
| templates/__init__.py                                          |     2 |    1 |     1 |    50% |
| templates/_template_errors.py                                  |    11 |    0 |    11 |   100% |
| templates/base_metadata_extractor.py                           |     4 |    1 |     3 |    75% |
| templates/incremental_sql_metadata_extractor.py                |    21 |    2 |    19 |    90% |
| templates/sql_app.py                                           |    50 |    6 |    44 |    88% |
| templates/sql_app_errors.py                                    |     9 |    0 |     9 |   100% |
| templates/sql_metadata_extractor.py                            |    14 |    1 |    13 |    93% |
| templates/sql_query_extractor.py                               |     6 |    1 |     5 |    83% |
| templates/contracts/__init__.py                                |     1 |    0 |     1 |   100% |
| templates/contracts/base_metadata_extraction.py                |     3 |    0 |     3 |   100% |
| templates/contracts/incremental_sql.py                         |    32 |    7 |    25 |    78% |
| templates/contracts/sql_metadata.py                            |    37 |    9 |    28 |    76% |
| templates/contracts/sql_query.py                               |     7 |    0 |     7 |   100% |
| test_utils/integration/__init__.py                             |     1 |    1 |     0 |     0% |
| testing/__init__.py                                            |     2 |    0 |     2 |   100% |
| testing/_errors.py                                             |     5 |    0 |     5 |   100% |
| testing/_mustache.py                                           |     2 |    0 |     2 |   100% |
| testing/fake_source.py                                         |    60 |   17 |    43 |    72% |
| testing/fixtures.py                                            |    12 |    0 |    12 |   100% |
| testing/golden.py                                              |    26 |   10 |    16 |    62% |
| testing/mocks.py                                               |    68 |   17 |    51 |    75% |
| testing/preflight.py                                           |    20 |    6 |    14 |    70% |
| testing/setup_routes.py                                        |    45 |    5 |    40 |    89% |
| testing/volatile_fields.py                                     |     1 |    0 |     1 |   100% |
| testing/warming.py                                             |    11 |    4 |     7 |    64% |
| testing/e2e/__init__.py                                        |     1 |    0 |     1 |   100% |
| testing/e2e/_errors.py                                         |    18 |    0 |    18 |   100% |
| testing/e2e/_manifest_identity.py                              |    11 |    0 |    11 |   100% |
| testing/e2e/base.py                                            |   137 |    7 |   130 |    95% |
| testing/e2e/client.py                                          |    29 |    7 |    22 |    76% |
| testing/e2e/config.py                                          |     5 |    0 |     5 |   100% |
| testing/e2e/credential.py                                      |     2 |    0 |     2 |   100% |
| testing/e2e/logs.py                                            |     9 |    1 |     8 |    89% |
| testing/e2e/payload.py                                         |    11 |    0 |    11 |   100% |
| testing/e2e/portforward.py                                     |     1 |    0 |     1 |   100% |
| testing/e2e/sql_app.py                                         |    10 |    0 |    10 |   100% |
| testing/e2e/substitutions.py                                   |     3 |    0 |     3 |   100% |
| testing/e2e/system_app.py                                      |    10 |    0 |    10 |   100% |
| testing/e2e/tenant_pool.py                                     |     3 |    0 |     3 |   100% |
| testing/e2e/workflows.py                                       |     6 |    1 |     5 |    83% |
| testing/full_dag/__init__.py                                   |     1 |    0 |     1 |   100% |
| testing/full_dag/_errors.py                                    |     1 |    0 |     1 |   100% |
| testing/full_dag/base.py                                       |    18 |    2 |    16 |    89% |
| testing/full_dag/client.py                                     |     1 |    0 |     1 |   100% |
| testing/full_dag/payload.py                                    |     8 |    0 |     8 |   100% |
| testing/full_dag/sql_app.py                                    |     6 |    1 |     5 |    83% |
| testing/harness/__init__.py                                    |     1 |    0 |     1 |   100% |
| testing/harness/_errors.py                                     |    13 |    0 |    13 |   100% |
| testing/harness/_poll.py                                       |    15 |    0 |    15 |   100% |
| testing/harness/bridge.py                                      |     5 |    0 |     5 |   100% |
| testing/harness/budgets.py                                     |     6 |    0 |     6 |   100% |
| testing/harness/evidence.py                                    |    15 |    1 |    14 |    93% |
| testing/harness/expectations.py                                |    41 |    1 |    40 |    98% |
| testing/harness/fixtures.py                                    |    43 |    4 |    39 |    91% |
| testing/harness/identity.py                                    |    11 |    1 |    10 |    91% |
| testing/harness/outcome.py                                     |    16 |    0 |    16 |   100% |
| testing/harness/preconditions.py                               |    19 |    4 |    15 |    79% |
| testing/harness/spec.py                                        |     2 |    0 |     2 |   100% |
| testing/harness/substrate.py                                   |     3 |    0 |     3 |   100% |
| testing/harness/waiting.py                                     |     8 |    0 |     8 |   100% |
| testing/harness/atlas/__init__.py                              |    29 |    6 |    23 |    79% |
| testing/harness/atlas/_errors.py                               |     2 |    0 |     2 |   100% |
| testing/harness/automation_engine/__init__.py                  |     1 |    0 |     1 |   100% |
| testing/harness/automation_engine/_errors.py                   |    10 |    0 |    10 |   100% |
| testing/harness/automation_engine/client.py                    |    39 |    4 |    35 |    90% |
| testing/harness/automation_engine/retry.py                     |    17 |    1 |    16 |    94% |
| testing/harness/automation_engine/wire.py                      |    24 |    2 |    22 |    92% |
| testing/harness/cluster/__init__.py                            |     1 |    0 |     1 |   100% |
| testing/harness/cluster/_errors.py                             |     4 |    0 |     4 |   100% |
| testing/harness/cluster/_portforward.py                        |    15 |    2 |    13 |    87% |
| testing/harness/cluster/_protocols.py                          |     9 |    0 |     9 |   100% |
| testing/harness/cluster/_states.py                             |     9 |    0 |     9 |   100% |
| testing/harness/cluster/kube.py                                |    50 |    7 |    43 |    86% |
| testing/harness/seed/__init__.py                               |     5 |    0 |     5 |   100% |
| testing/harness/seed/_errors.py                                |     7 |    0 |     7 |   100% |
| testing/harness/seed/_ndjson.py                                |     7 |    0 |     7 |   100% |
| testing/harness/seed/_publish.py                               |     8 |    0 |     8 |   100% |
| testing/harness/seed/_spec.py                                  |    11 |    0 |    11 |   100% |
| testing/harness/starters/__init__.py                           |     2 |    0 |     2 |   100% |
| testing/harness/starters/_ae.py                                |     4 |    0 |     4 |   100% |
| testing/harness/starters/_errors.py                            |     4 |    0 |     4 |   100% |
| testing/harness/starters/_queue.py                             |     3 |    0 |     3 |   100% |
| testing/harness/starters/_specs.py                             |    12 |    0 |    12 |   100% |
| testing/harness/teardown/__init__.py                           |     5 |    0 |     5 |   100% |
| testing/harness/teardown/_dag.py                               |     4 |    0 |     4 |   100% |
| testing/harness/teardown/_purge.py                             |     8 |    0 |     8 |   100% |
| testing/harness/temporal/__init__.py                           |     1 |    0 |     1 |   100% |
| testing/harness/temporal/_errors.py                            |     5 |    0 |     5 |   100% |
| testing/harness/temporal/_protocols.py                         |     4 |    0 |     4 |   100% |
| testing/harness/temporal/_states.py                            |     6 |    0 |     6 |   100% |
| testing/harness/temporal/client.py                             |    23 |    1 |    22 |    96% |
| testing/hypothesis/__init__.py                                 |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/__init__.py                      |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/sql_client.py                    |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/clients/__init__.py              |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/clients/sql.py                   |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/common/__init__.py               |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/common/logger.py                 |     3 |    0 |     3 |   100% |
| testing/hypothesis/strategies/handlers/__init__.py             |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/handlers/sql/__init__.py         |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/handlers/sql/sql_metadata.py     |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/handlers/sql/sql_preflight.py    |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/inputs/__init__.py               |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/inputs/json_input.py             |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/inputs/parquet_input.py          |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/outputs/__init__.py              |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/outputs/json_output.py           |     2 |    1 |     1 |    50% |
| testing/hypothesis/strategies/outputs/statestore.py            |     3 |    1 |     2 |    67% |
| testing/hypothesis/strategies/server/__init__.py               |     1 |    1 |     0 |     0% |
| testing/hypothesis/strategies/server/fastapi/__init__.py       |     1 |    1 |     0 |     0% |
| testing/integration/__init__.py                                |     3 |    1 |     2 |    67% |
| testing/integration/_errors.py                                 |    17 |    0 |    17 |   100% |
| testing/integration/assertions.py                              |    55 |   25 |    30 |    55% |
| testing/integration/client.py                                  |    18 |    0 |    18 |   100% |
| testing/integration/comparison.py                              |    12 |    1 |    11 |    92% |
| testing/integration/corpus.py                                  |    25 |    7 |    18 |    72% |
| testing/integration/fixtures.py                                |    26 |    1 |    25 |    96% |
| testing/integration/lazy.py                                    |    10 |    0 |    10 |   100% |
| testing/integration/models.py                                  |     9 |    0 |     9 |   100% |
| testing/integration/runner.py                                  |    27 |    2 |    25 |    93% |
| testing/integration/source.py                                  |     8 |    0 |     8 |   100% |
| testing/integration/validation.py                              |     7 |    0 |     7 |   100% |
| testing/parity/__init__.py                                     |     1 |    0 |     1 |   100% |
| testing/parity/__main__.py                                     |     2 |    1 |     1 |    50% |
| testing/parity/comparator.py                                   |     8 |    0 |     8 |   100% |
| testing/parity/models.py                                       |     5 |    1 |     4 |    80% |
| testing/parity/report.py                                       |     4 |    0 |     4 |   100% |
| testing/scale_data_generator/__init__.py                       |     1 |    0 |     1 |   100% |
| testing/scale_data_generator/config_loader.py                  |    11 |    4 |     7 |    64% |
| testing/scale_data_generator/data_generator.py                 |    10 |    3 |     7 |    70% |
| testing/scale_data_generator/driver.py                         |     3 |    3 |     0 |     0% |
| testing/scale_data_generator/output_handler/__init__.py        |     1 |    1 |     0 |     0% |
| testing/scale_data_generator/output_handler/base.py            |     7 |    3 |     4 |    57% |
| testing/scale_data_generator/output_handler/csv_handler.py     |     6 |    6 |     0 |     0% |
| testing/scale_data_generator/output_handler/json_handler.py    |     5 |    5 |     0 |     0% |
| testing/scale_data_generator/output_handler/parquet_handler.py |     6 |    6 |     0 |     0% |
| testing/sdr/__init__.py                                        |     1 |    0 |     1 |   100% |
| testing/sdr/base.py                                            |    14 |    3 |    11 |    79% |
| tools/__init__.py                                              |     1 |    1 |     0 |     0% |
| tools/provision_credentials.py                                 |     2 |    1 |     1 |    50% |
| transformers/__init__.py                                       |     4 |    2 |     2 |    50% |
| transformers/errors.py                                         |     2 |    1 |     1 |    50% |
| transformers/atlas/__init__.py                                 |     6 |    1 |     5 |    83% |
| transformers/atlas/errors.py                                   |     8 |    7 |     1 |    12% |
| transformers/atlas/sql.py                                      |    25 |    4 |    21 |    84% |
| transformers/common/__init__.py                                |     1 |    1 |     0 |     0% |
| transformers/common/last_sync.py                               |     1 |    0 |     1 |   100% |
| transformers/common/utils.py                                   |     6 |    0 |     6 |   100% |
| transformers/query/__init__.py                                 |    19 |    2 |    17 |    89% |
| transformers/query/errors.py                                   |     5 |    3 |     2 |    40% |
| validation/__init__.py                                         |     1 |    0 |     1 |   100% |
| validation/artifacts.py                                        |    28 |    2 |    26 |    93% |
| validation/asset_rules.py                                      |     3 |    0 |     3 |   100% |
| validation/assets.py                                           |    33 |    2 |    31 |    94% |
| validation/interceptor.py                                      |    17 |    0 |    17 |   100% |
| validation/ndjson.py                                           |    28 |    4 |    24 |    86% |
| validation/parquet.py                                          |    16 |    0 |    16 |   100% |
| validation/protocols.py                                        |     9 |    0 |     9 |   100% |
| validation/sources.py                                          |    15 |    0 |    15 |   100% |
| validation/wrapper.py                                          |     8 |    0 |     8 |   100% |
|----------------------------------------------------------------|-------|------|-------|--------|
| TOTAL                                                          |  5409 | 1082 |  4327 |  80.0% |
---------------- RESULT: PASSED (minimum: 0.0%, actual: 80.0%) -----------------

@cmgrote

cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

/lens

@atlan-app-fleet

atlan-app-fleet Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

lens summary · updated after round 4 (re-review · only commits since cda08d0)

✅ Ready to merge — every finding is resolved

Open findings: 🔴 0 critical · 🟠 0 high · 🟡 0 medium · ⚪ 0 low (nit)

lens reads this PR as — Observability periodic flushes previously ran on several unrelated asyncio loops and threads, while Segment had its own worker loop, leaving no single lifecycle to coordinate their final drains. The PR aims to make scheduling consistent and place a bound on shutdown flushing. How: It adds a lazy process-wide background event loop, routes each sink's periodic flush and Segment's queue worker through it, and performs a staged, time-bounded shutdown: periodic tasks flush first, the Segment worker then drains, and other tasks on the loop are awaited rather than cancelled.

Approach check — worth a second look (advisory, does not block):

  • Clarify whether the new loop types are supported public API (A1) — application_sdk/observability/background_loop.py introduces non-underscored BackgroundTask and ObservabilityLoop types with public methods; the supplied API list marks these as public changes, but the description frames the module as internal observability plumbing and does not establish a supported caller contract. In-repo class call sites are limited (1 for BackgroundTask, 2 for ObservabilityLoop), and consumers in other repositories were not checked. Instead: If these are implementation details, expose them through a private module/names; otherwise document them as supported API and check external consumers before relying on their new contract.
✔️ Resolved (8)
id severity where finding found resolved how
F-1a231b ⚪ low tests/unit/observability/test_logger_adaptor.py:1580 Remove the redundant asyncio marker round 1 round 2 its code was removed or rewritten
F-9dec31 ⚪ low application_sdk/observability/traces_adaptor.py:89 Trace test reset leaves the old shared-loop task running round 1 round 2 its code was removed or rewritten
F-a63ae9 ⚪ low tests/unit/observability/test_background_loop.py:148 Synchronize the loop before claiming the start race round 1 round 2 its code was removed or rewritten
F-68ccaa 🟡 medium application_sdk/observability/background_loop.py:213 Shutdown can miss a task being registered round 1 round 3 verified fixed
F-74ce6d ⚪ low application_sdk/observability/background_loop.py:146 Fork reset leaves sink start guards inherited round 1 round 3 verified fixed
F-bc1d48 ⚪ low tests/unit/observability/test_background_loop.py:58 Exercise the advertised post-fork restart path round 1 round 3 verified fixed
F-ed4ad8 🟡 medium application_sdk/observability/segment_client.py:261 Shutdown can close Segment's client before the final metrics flush round 1 round 3 verified fixed
F-e6ddbc ⚪ low tests/unit/observability/test_traces_adaptor.py:244 Remove redundant asyncio marker round 1 round 4 its code was removed or rewritten
🕘 Round history (5)
round run commits new resolved blocking open cost log
1 first review 47d90aa..5f0628c 8 0 0 $0.131 run
2 re-review · only commits since 5f0628c 5f0628c..cda08d0 0 3 0 $0.031 run
3 re-review · full, because requested with force 47d90aa..cda08d0 0 4 0 $0.151 run
3 recheck · F-e6ddbc by @cmgrote cda08d0 0 0 0 $0.001 run
4 re-review · only commits since cda08d0 cda08d0..4876bc6 0 1 0 $0.000 run

ℹ️ Nothing was re-checked this round — none of the PR's own files changed since the last review (a merge from the base branch alone changes none), so A1 were not re-judged. They show as open because nothing checked them. Comment /lens force to re-check them against the whole PR, or /lens force verify-budget if the last re-check ran out of output budget.

round 4 · $0.313 of $1.00 · 147 model calls · 0 failed requests · 84% prompt cache hits · gpt-6-luna

…start flushes after fork [FND-4648]

- Stop the Segment worker only after the periodic flushes: the metrics
  final flush drains its queue through the HTTP client the worker closes.
- Schedule a started task under the loop lock, so a racing shutdown's
  drain always finds it.
- After fork, the child replaces the loop's lock, drops each Segment
  client's worker and restarts every sink's periodic flush on its own loop.
- Traces and metrics keep their flush handle and stop it on reset.
- Log the shutdown timeout with exc_info (conformance L004); bound the
  hang test with a pytest timeout instead of a wall-clock assert.
- Tests: staged stop, start/shutdown race, Segment drain on an open
  client, child flush after fork, traces reset; drop redundant asyncio
  markers.

Claude-Session: https://claude.ai/code/session_01AbKqGbSfZqfoW5hkNx3D61
@cmgrote

cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

/lens

@atlan-app-fleet

Copy link
Copy Markdown
Contributor

🟡 lens · round 2 · re-review · only commits since 5f0628c — not ready: 5 medium/low open (F-68ccaa, F-74ce6d, F-bc1d48…)

Open findings: 🔴 0 critical · 🟠 0 high · 🟡 2 medium · ⚪ 3 low (nit)
Fix them and comment /lens, or close one the team won't fix with /lens dismiss <id> <reason>.
Resolved this round: 3

⚠️ Verify ran out of output budget — the re-check of F-68ccaa, F-74ce6d, F-bc1d48, F-ed4ad8, F-e6ddbc, A1 spent all 8,000 output tokens before answering, so they were not re-judged this round. They show as open because nothing checked them, not because a check found them unfixed. Worth opting in: comment /lens force verify-budget to re-check them with 24,000. Keep force: without it, a round whose push changes none of the PR's files (a merge from the base branch) re-checks nothing; with it, they are re-checked whatever the push changed.

Approach check — ⚠️ concerns (advisory)

  • Problem: The observability sinks and Segment queue worker previously owned several different event loops or threads, with independent shutdown behavior and no single bound for their final flushes. The PR aims to centralize periodic observability work and bound its process-exit drain.
  • How the PR solves it: It adds a lazy, process-wide daemon event loop and task handles, routes logs, metrics, traces, sizing, and the Segment worker through it, and registers one bounded shutdown that cancels managed tasks for their final work while awaiting other tasks on that loop. The console writer and OTLP exporter remain on their existing mechanisms because they have different blocking or ownership characteristics.
  • ⚠️ Fork-awareness does not extend to already-initialized sinks (A1) — The PR describes the loop as “fork-aware,” and ObservabilityLoop._ensure_started_locked() resets the loop singleton in a child, but its callers retain inherited state. In segment_client.py, send_metric() skips _start_worker() while the inherited _worker.done() is false and then schedules onto the inherited _loop; likewise, the adapter _flush_task_started flags remain true, preventing periodic tasks from restarting. Thus a child that forks after sink initialization does not actually resume observability work on the fresh loop. Instead: Reset or recreate the loop-owned worker and periodic-sink state in the child process (for example, with per-process PID checks or a fork hook) so inherited sinks start on the child’s loop.

$0.162 of $1.00 · 71 model calls · 0 failed requests

Full summary · Run log

@cmgrote

cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

/lens force verify-budget

@atlan-app-fleet

atlan-app-fleet Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
38062 36083 95% 0% 🟢

New Files

File Coverage Status
application_sdk/observability/background_loop.py 83% 🟢
TOTAL 83% 🟢

Modified Files

File Coverage Status
application_sdk/constants.py 99% 🟢
application_sdk/observability/logger_adaptor.py 86% 🟢
application_sdk/observability/metrics_adaptor.py 92% 🟢
application_sdk/observability/observability.py 77% 🟢
application_sdk/observability/segment_client.py 87% 🟢
application_sdk/observability/sizing_sink.py 96% 🟢
application_sdk/observability/traces_adaptor.py 97% 🟢
TOTAL 90% 🟢

updated for commit: 4876bc6 by action🐍

@atlan-app-fleet

Copy link
Copy Markdown
Contributor

🟡 lens · round 3 · re-review · full, because requested with force — not ready: 1 medium/low open (F-e6ddbc)

Open findings: 🔴 0 critical · 🟠 0 high · 🟡 0 medium · ⚪ 1 low (nit)
Fix them and comment /lens, or close one the team won't fix with /lens dismiss <id> <reason>.
Resolved this round: 4

Approach check — ⚠️ concerns (advisory)

  • Problem: Observability periodic flushes previously ran on several unrelated asyncio loops and threads, while Segment had its own worker loop, leaving no single lifecycle to coordinate their final drains. The PR aims to make scheduling consistent and place a bound on shutdown flushing.
  • How the PR solves it: It adds a lazy process-wide background event loop, routes each sink's periodic flush and Segment's queue worker through it, and performs a staged, time-bounded shutdown: periodic tasks flush first, the Segment worker then drains, and other tasks on the loop are awaited rather than cancelled.
  • ⚠️ Clarify whether the new loop types are supported public API (A1) — application_sdk/observability/background_loop.py introduces non-underscored BackgroundTask and ObservabilityLoop types with public methods; the supplied API list marks these as public changes, but the description frames the module as internal observability plumbing and does not establish a supported caller contract. In-repo class call sites are limited (1 for BackgroundTask, 2 for ObservabilityLoop), and consumers in other repositories were not checked. Instead: If these are implementation details, expose them through a private module/names; otherwise document them as supported API and check external consumers before relying on their new contract.

$0.313 of $1.00 · 146 model calls · 0 failed requests

Full summary · Run log

@cmgrote

cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

/lens recheck F-e6ddbc Fixed in cda08d0: tests/unit/observability/test_traces_adaptor.py:244 is now async def test_with_a_running_loop_no_task_lands_on_it(self): with no @pytest.mark.asyncio above it (line 243 is blank). Same fix applied to test_logger_adaptor.py (F-1a231b, resolved).

@atlan-app-fleet

Copy link
Copy Markdown
Contributor

🟡 lens · re-check after round 3 · F-e6ddbc by @cmgrote — not ready: 1 medium/low open (F-e6ddbc)

Open findings: 🔴 0 critical · 🟠 0 high · 🟡 0 medium · ⚪ 1 low (nit)
Fix them and comment /lens, or close one the team won't fix with /lens dismiss <id> <reason>.

Approach check — ⚠️ concerns (advisory)

  • Problem: Observability periodic flushes previously ran on several unrelated asyncio loops and threads, while Segment had its own worker loop, leaving no single lifecycle to coordinate their final drains. The PR aims to make scheduling consistent and place a bound on shutdown flushing.
  • How the PR solves it: It adds a lazy process-wide background event loop, routes each sink's periodic flush and Segment's queue worker through it, and performs a staged, time-bounded shutdown: periodic tasks flush first, the Segment worker then drains, and other tasks on the loop are awaited rather than cancelled.
  • ⚠️ Clarify whether the new loop types are supported public API (A1) — application_sdk/observability/background_loop.py introduces non-underscored BackgroundTask and ObservabilityLoop types with public methods; the supplied API list marks these as public changes, but the description frames the module as internal observability plumbing and does not establish a supported caller contract. In-repo class call sites are limited (1 for BackgroundTask, 2 for ObservabilityLoop), and consumers in other repositories were not checked. Instead: If these are implementation details, expose them through a private module/names; otherwise document them as supported API and check external consumers before relying on their new contract.

$0.313 of $1.00 · 147 model calls · 0 failed requests

Full summary · Run log

  • ❌ F-e6ddbc still stands: The finding still applies: <code_now> shows @pytest.mark.asyncio immediately above async def test_send_to_otel() at lines 115–116. The author's cited change concerns a different test at line 244, so it does not remove this decorator. confirms test_send_to_otel exists on the merge base; no compatibility concern changes whether the marker is redundant under the root asyncio_mode = "auto" configuration stated in the finding.
    • present on merge base: test_send_to_otel in tests/unit/observability/test_traces_adaptor.py
    • public: module tests.unit.observability.test_traces_adaptor, symbol test_send_to_otel
    • not listed in docs/agents/sdk-capabilities.md
    • callers of test_send_to_otel at the PR head (0 in its file or importing it): none

…ptor tests [FND-4648]

asyncio_mode is auto, so pytest-asyncio collects these without the marker.

Claude-Session: https://claude.ai/code/session_01AbKqGbSfZqfoW5hkNx3D61
@cmgrote

cmgrote commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

/lens

@atlan-app-fleet

Copy link
Copy Markdown
Contributor

✅ lens · round 4 · re-review · only commits since cda08d0 — ready to merge — every finding resolved · $0.31

Open findings: 🔴 0 critical · 🟠 0 high · 🟡 0 medium · ⚪ 0 low (nit)
Resolved this round: 1

ℹ️ Nothing was re-checked this round — none of the PR's own files changed since the last review (a merge from the base branch alone changes none), so A1 were not re-judged. They show as open because nothing checked them. Comment /lens force to re-check them against the whole PR, or /lens force verify-budget if the last re-check ran out of output budget.

Approach check — ⚠️ concerns (advisory)

  • Problem: Observability periodic flushes previously ran on several unrelated asyncio loops and threads, while Segment had its own worker loop, leaving no single lifecycle to coordinate their final drains. The PR aims to make scheduling consistent and place a bound on shutdown flushing.
  • How the PR solves it: It adds a lazy process-wide background event loop, routes each sink's periodic flush and Segment's queue worker through it, and performs a staged, time-bounded shutdown: periodic tasks flush first, the Segment worker then drains, and other tasks on the loop are awaited rather than cancelled.
  • ⚠️ Clarify whether the new loop types are supported public API (A1) — application_sdk/observability/background_loop.py introduces non-underscored BackgroundTask and ObservabilityLoop types with public methods; the supplied API list marks these as public changes, but the description frames the module as internal observability plumbing and does not establish a supported caller contract. In-repo class call sites are limited (1 for BackgroundTask, 2 for ObservabilityLoop), and consumers in other repositories were not checked. Instead: If these are implementation details, expose them through a private module/names; otherwise document them as supported API and check external consumers before relying on their new contract.

$0.313 of $1.00 · 147 model calls · 0 failed requests

Full summary · Run log

@atlan-ci atlan-ci left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lens: ready to merge — every finding at every level is resolved (round 4).

@cmgrote
cmgrote added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit cdfbbdb Nov 9, 2026
48 checks passed
@cmgrote
cmgrote deleted the christophergrote/fnd-4648 branch November 9, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants