Skip to content

Add sender queue that expires oldest first - #986

Merged
StephenWakely merged 19 commits into
masterfrom
stephen/sender_queue
Sep 22, 2026
Merged

StephenWakely merged 19 commits into
masterfrom
stephen/sender_queue

Conversation

@StephenWakely

@StephenWakely StephenWakely commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Requirements for Contributing to this repository

  • Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
  • The pull request must only fix one issue, or add one feature, at the time.
  • The pull request must update the test suite to demonstrate the changed functionality.
  • After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. For more details, please see CONTRIBUTING.

What does this PR do?

Changes the sender queue from a simple queue.Queue to a queue with the following properties:

  • On adding to a full queue (after the configured timeout) the oldest item in the queue is dropped and the new item is added.
  • Un-timestamped metrics added to the queue will expire if they are in the queue for longer than 10 seconds.

Description of the Change

With the recent addition of a socket connect timeout, metrics can be retried whilst facing network outages or agent downtimes. As it is retrying the sender queue can fill up, and eventually will overflow. When overflowing we want to make sure the most relevant metrics are kept.

Metrics sent without a timestamp have a limited time during which they are relevant. If we queue metrics for 2 minutes, and then send them all in one go, this would lead to 2 minutes worth of metrics being aggregated in a single time window, causing a huge spike. Metric expiry ensures this cannot happen.

Metrics sent with timestamps cannot expire as they are sent through to the Datadog backend as is.

Alternate Designs

Possible Drawbacks

There are performance implications with this change. Benchmarking:

  • agent restart with 60 second restart time
  • clients sending 28,000 metrics/second

3 scenarios:

  • version 0.53 with timestamped metrics.
  • this PR with timestamped metrics
  • this PR with non-timestamped metrics

During the restart the client queue grows to ~217k. (Each item in the queue is a payload which can consist of more than one metric, nontimestamped queue is less because items start expiring earlier).
image

Memory usage is roughly the same at the peak - no timestamps is slightly less because the payloads don't include the timestamp string

image

Settling down to within ~30mb.

image

I don't get accurate cpu measurements during the agent downtime since the agent is down, but during typical usage CPU seems largely unaffected.

image

Dropped metrics

It is worth highlighting that this PR will cause more non-timestamped metrics to be dropped over the previous release since we now expire metrics that are stuck in a queue for more than 10 seconds if they are not sent with a timestamp.

image

Verification Process

Additional Notes

There is a followup PR:

  • Make the socket_connect_timeout a binary option. With the queue now dropping oldest metrics, we don't need to limit the time we wait for a reconnect. We continue attempting to reconnect until success.
  • Add timeout
  • add timeout bounds to wait_for_pending and stop functions.

Release Notes

Review checklist (to be filled by reviewers)

  • Feature or bug fix MUST have appropriate tests (unit, integration, etc...)
  • PR title must be written as a CHANGELOG entry (see why)
  • Files changes must correspond to the primary purpose of the PR as described in the title (small unrelated changes should have their own PR)
  • PR must have one changelog/ label attached. If applicable it should have the backward-incompatible label attached.
  • PR should not have do-not-merge/ label attached.
  • If Applicable, issue must have kind/ and severity/ labels attached at least.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved critical and moderate findings remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request adds a bounded sender queue that evicts oldest entries, expires stale metrics, and retries transient failures.

Changes:

  • Introduces SenderQueue and PendingPayload.
  • Integrates expiry, eviction telemetry, and retry handling.
  • Adds unit tests and a performance benchmark.
File summaries
File Review summary
tests/unit/dogstatsd/test_statsd.py One nit regarding a timing-dependent queue test.
tests/performance/test_sender_queue_benchmark.py Reviewed with no final comments.
datadog/dogstatsd/sender_queue.py One critical producer-notification issue and one moderate shutdown requeue issue.
datadog/dogstatsd/base.py One critical shutdown issue and three moderate expiry, batching, and retry issues.
Review details

Suppressed comments (2)

datadog/dogstatsd/base.py:1792

  • Queue-mode retry only handles sent is None, but _xmit_packet_attempt() returns False for socket.timeout; _get_uds_socket() can raise that exception when a UDS connect attempt times out. A connection timeout therefore falls through to the writer-drop path and permanently loses the queued payload instead of letting the sender queue retry it. Distinguish connect timeouts from send timeouts or propagate connect timeouts as transient failures in queue mode.
        if sent is None and queue_mode:
            # Connection trouble, and the caller is the background sender
            # queue: let it requeue the payload and retry once reconnected,
            # instead of dropping it here.
            return None

datadog/dogstatsd/sender_queue.py:196

  • Stop is allowed past the capacity limit in put(), but requeue_front() counts that sentinel in len(self._deque). During shutdown, if the sender has an in-flight payload and stop() appends Stop, a failed send sees the queue as full and drops the payload instead of requeueing it; this loses metrics that shutdown is supposed to drain. Coordinate sentinel insertion with in-flight work or make the requeue path ignore the shutdown sentinel when enforcing capacity.
            if self._maxsize > 0 and len(self._deque) >= self._maxsize:
                self._on_drop_queue_full(item)
                self._finish_task_locked()
                return
  • Files reviewed: 4/4 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datadog/dogstatsd/base.py
Comment on lines +2180 to +2184
if sent is None:
# Connection trouble: keep the payload for the next attempt
# instead of losing it. The queue's own expiry check (on a
# future get()) is what eventually gives up on a payload
# that's been stuck for too long, unless it's replay-safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will be addressed in a followup PR that will place a timeout on these functions.

Comment thread datadog/dogstatsd/sender_queue.py
Comment thread datadog/dogstatsd/base.py Outdated
Comment thread datadog/dogstatsd/base.py Outdated
Comment thread tests/unit/dogstatsd/test_statsd.py Outdated
@datadog-datadog-prod-us1-2

This comment has been minimized.

@StephenWakely
StephenWakely marked this pull request as ready for review September 14, 2026 13:24
@StephenWakely
StephenWakely requested review from a team as code owners September 14, 2026 13:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-14T13:30:39.226939Z dce1a3b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dce1a3b4e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread datadog/dogstatsd/base.py
# instead of losing it. The queue's own expiry check (on a
# future get()) is what eventually gives up on a payload
# that's been stuck for too long, unless it's replay-safe.
pending_queue.requeue_front(item) # type: ignore[arg-type]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Let shutdown terminate replay-safe retries

When a replay-safe payload encounters a transient UDS failure with a positive socket_connect_timeout, it is requeued at the front forever because it never expires. If stop(), disable_background_sender(), or pre_fork() appends Stop during the outage, this payload remains ahead of the sentinel on every retry, so _sender_thread.join() never returns and the application cannot shut down or fork. Shutdown needs to stop retrying or otherwise prioritize the sentinel.

Useful? React with 👍 / 👎.

@StephenWakely StephenWakely Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be addressed in next PR, #986.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

uhmmm, isn't this important enough to be fixed here? I mean, the fact that an application hangs when being shutdown is not great :D

@StephenWakely StephenWakely Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

uhmmm, isn't this important enough to be fixed here? I mean, the fact that an application hangs when being shutdown is not great :D

🤔 I can merge the two PRs if you feel it would be better? Just wanted to keep concerns separate..

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Up to you! Approved! Definitely you have more context here :)

Comment thread datadog/dogstatsd/base.py
Comment on lines +2191 to +2193
pending_queue.requeue_front(item) # type: ignore[arg-type]
time.sleep(backoff)
backoff = min(backoff * 2, UDS_CONNECT_RETRY_MAX_BACKOFF)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid backing off after requeue drops the payload

When the bounded queue is refilled while a send is in flight, or when that payload expires during the send attempt, requeue_front() drops it and completes its task rather than requeuing it. The caller cannot distinguish that outcome and still sleeps and increases backoff, delaying already-queued work by up to a second and making additional payloads more likely to expire under load; have requeue_front() report whether it retained the item and only back off on success.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I disagree with this. When the connection is down this indicates a downstream issue and we want to backoff the connection attempts regardless of the state of the items on the queue.

_send_to_buffer/_should_flush indexed {False:..., True:...} dicts by
replay_safe, costing a bool() coercion plus a subscript on the single
hottest path in the library (once per metric, ~18k/s per benchmark
client). Measured in the benchmark image: _send_to_buffer 0.6388us on
master -> 0.7344us on this branch (+15.0%), which was ~72% of the whole
per-metric regression (+0.132us, +7.2%).

Hold the two batches as four plain attributes and branch on replay_safe
instead, and inline the size comparison so the hot path makes no call to
_should_flush (which master also paid). _should_flush is retained for the
pre-existing surface and for tests.

Also stop reading the clock in SenderQueue.get() for entries that can
never expire: the argument to _expired() is evaluated before the call, so
every bare-string (replay-safe) get() computed a monotonic() it then
discarded.

Measured after, best-of-7: _send_to_buffer -16.5%, full per-metric -8.5%.
Unit tests: 184 passed, 1 skipped with DD_ORIGIN_DETECTION_ENABLED=false
(the 45 failures otherwise seen are a pre-existing container-id artifact
of the dev host, identical before and after this change).
These four are development aids, not part of the change under review:

  tests/manual/emulate_reconnect_then_write_fails.py
  tests/manual/test_sender_queue_manual.py
  tests/manual/test_shutdown_bound.py
  tests/performance/test_sender_queue_benchmark.py

They are ad-hoc drivers and a benchmark harness rather than tests the suite
runs, and together they accounted for 1278 of the ~1970 added lines under
tests/, which buried the unit coverage that does matter. Retained locally
via .git/info/exclude (which is not committed) so they stay available for
development without shipping in the review.
tests/manual/ holds ad-hoc development drivers, not tests the suite runs, so
none of it belongs in the review. This removes the one remaining tracked file,
test_gauge_with_timestamp_aggregation.py, leaving tests/manual/ entirely
untracked. Retained on disk via .git/info/exclude, which now excludes the
whole directory so future scratch scripts there cannot be added by accident.
@github-actions github-actions Bot added the documentation Documentation related changes label Sep 18, 2026
@StephenWakely StephenWakely added the changelog/Added Added features results into a minor version bump label Sep 18, 2026
nogates
nogates previously approved these changes Sep 18, 2026
Comment thread datadog/dogstatsd/base.py
"Gave up reconnecting after socket_connect_timeout (%ss), dropping the packet",
self.socket_connect_timeout,
)
sent = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Don't we actually requeue packet here instead of dropping?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is all removed in the followup PR where we no longer give up.

item = self._deque.popleft()
# A slot just opened up: wake one thread blocked in put()'s
# wait-for-room loop, if any (harmless no-op otherwise).
self._not_full.notify()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can here be a potential race condition with requeue_front?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe. There is an assumption that get and requeue_front have to be on the same thread. With a single sender thread that we have here, they are. There's nothing stopping someone coming along and breaking this in future. I'll look into that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This checks to ensure we don't double count.. 18d3018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/Added Added features results into a minor version bump documentation Documentation related changes mergequeue-status: done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants