Skip to content

fix(EVBL-47): disable pending-cert retries + always log ACME rate-limit context - #370

Open
evan-datadog wants to merge 11 commits into
masterfrom
evan/EVBL-47/le-pending-cert-no-retry
Open

fix(EVBL-47): disable pending-cert retries + always log ACME rate-limit context#370
evan-datadog wants to merge 11 commits into
masterfrom
evan/EVBL-47/le-pending-cert-no-retry

Conversation

@evan-datadog

@evan-datadog evan-datadog commented Aug 18, 2026

Copy link
Copy Markdown

Problem

When fetch_acme_cert fails, Lemur re-queues the task up to ACME_ADDITIONAL_ATTEMPTS times. For a deterministic failure (broken DNS delegation, no DNS provider, bad credentials, invalid identifier), retrying can never succeed so it exhausts the CA's rate limit.

Separately, the failure log only carried pending_cert_id, cn, and last_error, making rate-limit burn hard to attribute to a specific cert / domain / authority.

…imit context

Retries were burning the CA's ACME rate limit (Let's Encrypt: 5 duplicate
certs / failed validations per week per domain). A deterministic failure
(broken DNS delegation, no DNS provider, bad creds, invalid identifier) can
never succeed on retry, so each re-queue just created another ACME order /
failed validation against that budget.

- Set ACME_ADDITIONAL_ATTEMPTS = 0 and change the give-up condition to
  number_attempts >= ACME_ADDITIONAL_ATTEMPTS, so a pending cert is attempted
  exactly once and, on failure, marked resolved immediately (no re-queue).
- Always emit the full ACME rate-limit context on a failure log (cn, authority,
  authority_id, number_attempts, dns_provider_id, last_error, and a
  rate_limit_relevant flag) so rate-limit burn is attributable to a specific
  cert / domain / authority for triage.

Adds tests covering the no-re-queue behavior and the rate-limit log context.

Workspace: local
@evan-datadog
evan-datadog requested review from a team as code owners August 18, 2026 16:00
@datadog-official

This comment has been minimized.

… flakiness

g is app-context scoped and persists across tests, so a User set by an
authenticated request in one test leaks into later tests as a stale, detached
(and expired) instance. Accessing its attributes then raises DetachedInstanceError
in audit_log / certificate schema serialization. Clear g.current_user in the
session fixture teardown so each test starts clean.

This is needed for CI to pass reliably on master-based branches (same fix as
PR #368).

Workspace: local
current_app.logger.error() is silently dropped in the celery worker: the
worker's _configure_worker_logging clears app.logger.handlers and the log
propagates to an unhandled root logger, so no ERROR-level line is emitted
(confirmed: zero ERROR logs in celery-worker). The print() and task-trace
lines appear only because they go through Celery's own loggers.

Log the rate-limit failure via a module-level logger (logging.getLogger(__name__)),
which emits through Celery's configured handlers, so the rate_limit_relevant
error with cn/authority/number_attempts/dns_provider_id/last_error actually
surfaces for triage.

Workspace: local
…anch

Address review-swarm nits on PR #370:
- Log a meaningful default ('No error message provided by CA') instead of
  str(None) when last_error is missing, and guard the str() conversion.
- Add a clarifying comment that with ACME_ADDITIONAL_ATTEMPTS = 0 the
  number_attempts >= 0 condition is always true on the first failure, so the
  re-queue (else) branch is dead code retained only for future re-enabling.

Adds a test for the missing-last_error default.

Workspace: local
Comment thread lemur/common/celery.py Outdated
@evan-datadog
evan-datadog requested a review from maperu August 19, 2026 16:49
The claim that current_app.logger.error() is dropped in the Celery worker
was unverified. _configure_worker_logging clears app.logger.handlers to
prevent double-logging, but app.logger still propagates to root, which
Celery has configured — same path as the module logger. All other
current_app.logger calls in this file work correctly via propagation.

Tests confirm current_app.logger.error emits the rate-limit error log.

Workspace: local
Comment thread lemur/constants.py Outdated
# marked resolved immediately. Every failed issuance consumes the CA's ACME rate
# limit (e.g. Let's Encrypt: 5 duplicate certs / failed validations per week per
# domain), so re-queuing a deterministic failure just burns that budget.
ACME_ADDITIONAL_ATTEMPTS = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If we’ve agreed pending-certificate failures are not retried and should instead be resolved with clear error logging/notifications, why keep a retry constant and an effectively dead requeue branch? Setting this to 0 obscures the intended behavior and also affects unrelated synchronous issuance. I’d remove the retry logic from fetch_acme_cert entirely. If create_certificate_immediately still needs retries, it should use its own clearly named constant. Reintroducing pending retries later should be a deliberate code change, not a constant bump.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

removed it alltogether; we dont use create_certificate_immediately and if we do I dont think we need the retries for the same reason.

With ACME_ADDITIONAL_ATTEMPTS=0 the else (re-queue) branch in
fetch_acme_cert and the cli fetch loop was already dead code on every
failure. Remove it explicitly:

- celery.py: unconditionally notify + resolve on failure; drop the
  if/else and the ACME_ADDITIONAL_ATTEMPTS import
- cli.py: same — unconditional fail-fast; drop import
- challenge_types.py: remove @Retry(stop_max_attempt_number=0) decorator
  from create_certificate_immediately (stop_max_attempt_number=0 already
  meant no retries); remove unused retrying import
- constants.py: delete ACME_ADDITIONAL_ATTEMPTS

number_attempts DB column and schema field are left in place — the field
is still emitted in error logs for triage context.

Workspace: local
On failure, re-queue fetch_acme_cert up to 2 times (number_attempts 0
and 1). On the third failure (number_attempts >= 2) mark resolved and
notify the owner. Mirrors the same logic in the pending-cert CLI.

Tests cover: first failure re-queues, second failure re-queues, third
failure resolves + notifies, rate-limit context logged, missing
last_error falls back to default message.

Workspace: local
@evan-datadog
evan-datadog requested a review from maperu August 20, 2026 20:21
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