Skip to content

Data race: Arrow IPC trailers are Set from the stream-writer goroutine while fasthttp serialises the response header #729

Description

@xe-nvdk

Found while adding end-to-end coverage for #724. This is pre-existing and affects the trailers shipped by #721/#722, not just the new one.

What races

executeQueryArrow sets its trailers from inside the SetBodyStreamWriter callback:

That callback runs on the goroutine fasthttp starts in NewStreamReader (stream.go:42). Meanwhile the connection goroutine serialises the response head: Response.Write → writeBodyStream → ResponseHeader.Write → AppendBytes → appendStatusLine → formatStatusLine.

Both mutate the same ResponseHeader scratch buffer. formatStatusLine writes h.bufKV.value; Set → initHeaderKV writes h.bufKV. Race detector output:

WARNING: DATA RACE
Write at 0x00c0003f0d80 by goroutine 34:
  fasthttp.initHeaderKV()        header.go:3254
  fasthttp.(*ResponseHeader).Set() header.go:1489
  api.(*QueryHandler).executeQueryArrow.func5()  query_arrow.go (trailer Set)
  fasthttp.NewStreamReader.func1()  stream.go:44

Previous write at 0x00c0003f0d80 by goroutine 32:
  fasthttp.formatStatusLine()    status.go:167
  fasthttp.(*ResponseHeader).appendStatusLine() header.go:2366
  fasthttp.(*ResponseHeader).Write()  header.go:2307
  fasthttp.(*Response).writeBodyStream() http.go:1982

Why there is no happens-before edge

NewStreamReader uses fasthttputil.NewPipeConns(), which is buffered: a write from the stream writer does not block waiting for the connection goroutine to read, so it establishes no ordering against the earlier h.Write(w). I confirmed this by making the stream writer write and flush a byte before the Set; the race still fires.

The other direction is fine: Set → bw.Flush() → pw.Close() → reader EOF → writeTrailer is properly ordered, which is why trailer values arrive intact in practice.

Why CI does not catch it today

No test drives executeQueryArrow to its success path over a real HTTP response. TestExecuteQueryArrowReturnsConnectionOnPanic goes through the panic path, and that path deliberately does not set trailers. #722's own comment records the race for the panic path:

By this point fasthttp's connection goroutine may already be serialising the response header, and writing a trailer from this goroutine races with it (caught by the race detector).

The same race exists on the normal path and was never exercised.

Impact

Concurrent writes into a shared slice while the status line is being formatted. Observable as a corrupted status line or corrupted trailer key/value on an Arrow IPC response. Timing-dependent and rare, but not excluded by the memory model.

Why it blocks coverage

Any end-to-end success-path test of POST /api/v1/query/arrow trips it, because Arc-Execution-Time-Ms is set unconditionally. #724 therefore ships with unit coverage of its trailer constant and wiring but no end-to-end assertion that the handler sets it. Fixing this unblocks that test.

Possible directions

There is no race-free way to set a trailer value from the stream-writer goroutine with the fasthttp 1.51 API, since the value has to land in the same ResponseHeader the connection goroutine is serialising. Options worth weighing:

  1. Move the signals out of trailers and into the body where the format allows. Not possible for Arrow IPC, which has no envelope.
  2. Carry them in pre-body response headers where the value is known before streaming. Works for Arc-Rows-Capped only as "a cap applies", not "the cap was reached".
  3. Upstream a synchronised trailer-set API in fasthttp.
  4. Serialise access with our own mutex around every ResponseHeader mutation, which only helps if fasthttp's own writes take the same lock. They do not, so this does not actually fix it.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions