Skip to content

Retry the connection if a socket error is because the server went away. - #979

Open
StephenWakely wants to merge 6 commits into
masterfrom
stephen/retryconnection
Open

Retry the connection if a socket error is because the server went away.#979
StephenWakely wants to merge 6 commits into
masterfrom
stephen/retryconnection

Conversation

@StephenWakely

Copy link
Copy Markdown
Contributor

What does this PR do?

Retries reconnecting and resending a payload on send errors socket error.

Description of the Change

On transient send errors - errno.ECONNREFUSED, errno.ECONNRESET, errno.ENOTCONN, errno.EPIPE, errno.ENOENT, close the socket and attempt to resend. We only attempt resending once to avoid getting stuck in a loop if the agent doesn't come back beyond the socket_connect_timeout.

Alternate Designs

Possible Drawbacks

Verification Process

Additional Notes

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.

@StephenWakely
StephenWakely requested review from a team as code owners August 10, 2026 13:27
@StephenWakely StephenWakely added the changelog/Changed Changed features results into a major version bump label Aug 11, 2026
…empt

A single extra send attempt only rescues a packet if the reconnect happens
to land in the instant the peer comes back. For UDS SOCK_DGRAM, connect()
succeeds immediately against a stale-but-still-present socket path even
while nothing is listening, so the real failure only surfaces at send()
time - meaning the one-shot retry almost never helps during a real outage
(e.g. an agent restart). Retry submitting a packet with the same
exponential backoff already used for the initial UDS connect, bounded by
socket_connect_timeout, so it can actually ride out a longer outage.
@datadog-official

This comment has been minimized.

_socket_lock only guarded socket creation/teardown (get_socket()/
close_socket()) and, for SOCK_STREAM, the send itself - the SOCK_DGRAM
send and the socket-fetch that precedes it were unprotected. A thread
calling close_socket() (e.g. after its own failed send) could close the
fd while another thread already held a reference to it and was mid-send,
raising EBADF and dropping that packet with no chance to retry.

Widen _socket_lock to cover the whole fetch-and-send in
_xmit_packet_attempt, and make it an RLock since that now nests with the
lock already taken inside get_socket()/close_socket().
Comment thread datadog/dogstatsd/base.py Outdated
except socket.timeout:
# dogstatsd is overflowing, drop the packets (mimics the UDP behaviour)
pass
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This return bypasses the SOCK_STREAM cleanup below, which is why test_stream_cleanup is failing in ci.

Comment thread datadog/dogstatsd/base.py
self.socket_connect_timeout,
)
break
time.sleep(min(backoff, remaining))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we check the deadline after this sleep? Otherwise we might loop again even after expiration.

Comment thread datadog/dogstatsd/base.py Outdated
log.debug(
"Connection error submitting packet: %s, reconnecting and retrying", socket_err
)
self.close_socket()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should self.close_socket() be called earlier before _socket_lock is released to avoid another sender using the failed socket?

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.

Good catch!

Comment thread datadog/dogstatsd/base.py
# getting a fresh full socket_connect_timeout allowance each time.
connect_timeout = self.socket_connect_timeout
if retry_deadline is not None:
connect_timeout = retry_deadline - time.time()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the timeout be calculated after acquiring the lock?

Comment thread datadog/dogstatsd/base.py
def _xmit_packet(self, packet, is_telemetry):
# type: (str, bool) -> bool
retry_deadline = None
if self.socket_connect_timeout and self.socket_connect_timeout > 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to gate this to be for UDS only?

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

Labels

changelog/Changed Changed features results into a major version bump

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants