Skip to content

Trace GCS upload/download duration and bytes - #333

Closed
morgan-wowk wants to merge 1 commit into
masterfrom
tangle-inv-26772-gcs-tracing
Closed

Trace GCS upload/download duration and bytes#333
morgan-wowk wants to merge 1 commit into
masterfrom
tangle-inv-26772-gcs-tracing

Conversation

@morgan-wowk

@morgan-wowk morgan-wowk commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Why

A recent incident had Tangle pipelines stalling on GCS transfers. The first fix attempt was per-request timeouts (#330, now closed), but the stock google-cloud-storage client already enforces a 60s per-request timeout plus a ~120s retry deadline, so a same-default override added little. Before choosing any timeout policy we need to know how long real uploads/downloads take on prod — so we can find concretely where the time goes and which operation to focus on.

Out of scope

GCS Fuse operations, which is the more impactful area to measure, is not covered by this change.

Changes

  • New cloud_pipelines_backend/instrumentation/gcs_tracing.py: TracingGoogleCloudStorageProvider, a subclass of the upstream GoogleCloudStorageProvider that overrides only the four public transfer methods (upload, upload_bytes, download, download_bytes). Each wraps super() in an OpenTelemetry span.
  • The GKE launchers now construct the tracing provider instead of the plain one (one-line swap at each site; the positional gcs_client argument is unchanged).

Each span uses the tangle.storage tracer, is named gcs.<operation>, and carries:

  • gcs.operationupload / upload_bytes / download / download_bytes
  • gcs.uri — the object/prefix being transferred
  • gcs.bytes — payload size, recorded only for the in-memory transfers (upload_bytes / download_bytes, where it is a free len(data))

On failure the span is marked ERROR and the exception is recorded, then re-raised unchanged.

Duration is the primary signal and is intrinsic to every span. We deliberately do not walk the local filesystem to size file/dir transfers: that scan is strictly dominated by the transfer it would measure (the base provider already does one GCS round-trip per file), it added a robustness hole (a stat race could fail an otherwise-successful transfer), and authoritative sizes for those artifacts already live in artifact_data.total_size and can be joined offline.

System / UX impact

  • Measurement only — no transfer behavior changes; __init__ is not overridden and no timeouts are introduced.
  • Adds one span per GCS transfer (exported via the already-configured OTLP BatchSpanProcessor). No extra filesystem access.

Before / after

  • Before: GCS transfer latency was invisible; we could only guess where time went.
  • After: every launcher-side upload/download emits a span with its duration, queryable in Observe by operation and URI.

Follow-up

Companion oasis-backend PR bumps the submodule and swaps the orchestrator-side provider so orchestrator-initiated GCS ops (exists, get_info, small download_bytes, output preservation) are covered too.

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@morgan-wowk
morgan-wowk marked this pull request as ready for review August 17, 2026 19:58
@morgan-wowk
morgan-wowk requested a review from a team August 17, 2026 19:58
@morgan-wowk
morgan-wowk requested a review from Ark-kun as a code owner August 17, 2026 19:58
@morgan-wowk
morgan-wowk force-pushed the tangle-inv-26772-gcs-tracing branch from 8e80779 to 797c68e Compare August 17, 2026 20:01
Comment thread cloud_pipelines_backend/instrumentation/gcs_tracing.py Outdated
Emit one OpenTelemetry span per GCS transfer so we can see, on prod,
how long uploads and downloads actually take and for how many bytes.
Each span (tangle.storage tracer, name gcs.<operation>) carries
gcs.operation, gcs.uri, and gcs.bytes, and is marked ERROR with the
exception recorded on failure.

TracingGoogleCloudStorageProvider subclasses the upstream provider and
overrides only the four public transfer methods; the GKE launchers
construct it in place of the plain provider. This is measurement only:
it changes no transfer behavior and lets us find where latency
concentrates before deciding on any timeout policy.
@morgan-wowk
morgan-wowk force-pushed the tangle-inv-26772-gcs-tracing branch from 797c68e to db1c13e Compare August 17, 2026 20:16
yield span
except Exception as exception:
span.set_status(StatusCode.ERROR)
span.record_exception(exception)

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.

AI review: start_as_current_span() already records escaping exceptions and marks the span as ERROR by default. Because this handler also calls set_status() and record_exception() before re-raising, each failed transfer emits two exception events (confirmed with an in-memory span exporter). Could we remove the manual exception handling, or explicitly disable the context manager’s automatic handling?

@Mbeaulne Mbeaulne 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.

AI review left some comments. If they are true, fix. Otherwise I've approved

@camielvs camielvs 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.

I don't have enough python & backend context to review on an implementation level, but overall the intent looks fine to me

@morgan-wowk

Copy link
Copy Markdown
Collaborator Author

Closing in favor of orchestrator-side telemetry.

The inv-26772 investigation showed the orchestrator freeze happens in internal_process_one_running_execution, where get_reader().get_info() walks every file of a large Directory artifact on the single orchestrator thread. That is not on the path this PR instruments — the provider upload/download/upload_bytes/download_bytes methods here are the small control-plane transfers; the bulk artifact I/O goes through gcsfuse mounts, and the file-tree walk that actually blocks the loop goes through get_info(), which these overrides do not wrap.

Superseded by a stack that instruments the actual freeze path, attributed by container_execution.id:

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.

3 participants