Skip to content

ateapi: one bad actor row fails the whole ListActors response, with no indication which actor caused it #1401

Open

Description

@mayawang

Expected Behavior

A single actor whose serialized form cannot be decoded by a caller should cost that caller that
actor
— an error naming it, a skipped row, a partial response with a warning. Losing the view of
every actor in the atespace is disproportionate to the fault.

Actual Behavior

ListActors returns a single message containing every actor, so any decode failure on any row fails
the entire response for every caller. Observed as:

Error: failed to list actors: rpc error: code = Internal desc = grpc:
failed to unmarshal the received message: string field contains invalid UTF-8

There is nothing in the error identifying the actor, so recovery starts with an unbounded search
across the atespace. In the case that produced this (see the schema issue above), the trigger was
one running actor among many, and the only way to find it was to decode the wire bytes by hand.

Steps to Reproduce the Problem

  1. Produce any actor whose row a given client cannot decode. The reproducible route today is the tag
    reuse described in the linked issue: an old client plus one RUNNING actor with a
    worker_assignment of 128 bytes or more.
  2. Call ListActors for that atespace, e.g. kubectl-ate get actors -a <atespace>.
  3. Observe that the response fails entirely, that no other actor is listed, and that nothing in the
    error names the responsible actor.

Specifications

  • Version: main at ea3bdc32 (2026-09-02); rpc ListActors at ateapi.proto:102, response
    ListActorsResponse at :1588.
  • Platform: any.

Notes

Worth fixing on its own merits regardless of the schema issue: any future decode-affecting change,
on any field, inherits this blast radius. Pagination already exists on this RPC, so per-page partial
success plus a per-row error list is a natural shape.

Related to #1396

Activity

  1. cloudonly commented on Sep 29, 2026

    @cloudonly
    Contributor

    Looked into where this actually breaks; there are two layers with different owners.

    Layer 1 — the trigger in the report is client-side, and no server change fixes it. Actors are stored as proto bytes and the server decodes rows with DiscardUnknown (atepg.go:275), which succeeds for anything a newer schema wrote — in the current descriptor the fields involved are messages, not strings, so the server never produces an invalid string. The failure happens in old clients: descriptors generated before #737 (2026-08-04) read tags 5/6/7 as string (ateom_pod_*), and a worker_assignment of 128+ bytes makes the length varint start with 0x82 — not a valid UTF-8 start byte — so proto3 unmarshal rejects the whole response. (Between #737 and #1025 only tag 6 was still string; those clients misread silently rather than failing.) Rebuilding the client is the only fix for this layer.

    Layer 2 — server-visible bad rows fail the page unnamed, and the name is in scope. When a stored row fails to decode under the current descriptor, actor.go:208 and :256 fail the whole page with unmarshaling actor and no name — same shape in actor_template.go:189, atespace.go:102, tag.go:94/140, and worker_assignment.go:247.

    Two-step proposal, if there's interest:

    1. Name the row (no proto change): include atespace/name in those six error sites. Cheap, and turns recovery from an unbounded search into "look at that actor".
    2. Per-row isolation (proto shape, needs a nod): ListActorsResponse gains repeated RowError { ObjectRef actor = 1; string error = 2; }; the store skips rows that fail to decode and reports them; kubectl-ate prints them. One subtlety worth deciding up front: has-more currently follows len(decoded) > page_size (actor.go:218/266) — with skipped rows it has to follow the number of rows scanned, or a page containing two bad rows can emit an empty token and silently truncate the list. And if the shape is agreeable: apply it to all six List RPCs, or start with ListActors?

    Maya Wang (@mayawang) — you mentioned in #1396 splitting the ListActors per-row isolation out; if you're still planning to take that, please read the above as input rather than a land grab. Otherwise I'm happy to send the step-1 PR (and step 2 once the shape is settled). Either way it should stay coordinated with #1204 (also touching atepg actor storage) and #1607 (a new field is a non-breaking change for the guard).

  2. ericcurtin commented on Sep 30, 2026

    @ericcurtin
    Contributor

    Sent step 1 (name the row) in #2000. Step 2 (per-row isolation) is untouched.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiUser-facing API changeskind/bugSomething isn't working / bugfixesprio/P1Important but not critical

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions