Skip to content

THRIFT-6204: Omit an unset default-requiredness field from the Go writer - #3827

Draft
slachiewicz wants to merge 2 commits into
apache:masterfrom
slachiewicz:THRIFT-6204
Draft

THRIFT-6204: Omit an unset default-requiredness field from the Go writer#3827
slachiewicz wants to merge 2 commits into
apache:masterfrom
slachiewicz:THRIFT-6204

Conversation

@slachiewicz

Copy link
Copy Markdown
Member

JIRA: THRIFT-6204
Client: go

Stacked on #3822; the first commit is that PR. Review the second one.

Default requiredness means "write if set" (doc/specs/idl.md), and Python, Java, Node.js and C# all omit such a field when it holds no value. The Go writer wrote it unconditionally, so a nil pointer field either panicked on the way out — that half is #3822 — or, when its struct had no members, went onto the wire as an empty struct. Guarding the write with the generated IsSet helper, as optional fields already are, is a two-line change.

This is a wire change, and that is the whole question. Opening it as a draft because that is a maintainer's call, not mine.

What moves

A nil field whose struct has no members is written today and omitted after:

before:  0c 00 01 00 08 00 02 00 00 00 07 00    field 1 present, empty struct
after:         08 00 02 00 00 00 07 00          field 1 absent

A peer that declares that field required accepts the first and rejects the second:

before bytes -> err=<nil>
after bytes  -> err=Required field E is not set

Nothing panicked in that case, so this is working code changing behaviour, not a crash being fixed.

Blast radius

is_pointer_field() is true for every struct, union and exception field whatever its requiredness, and for every cpp.ref field, so the guard reaches wider than unions. Regenerating every IDL under test/, lib/go/test/ and tutorial/ with and without it:

Measure Count
generated files changed 75 of 650
write sites newly guarded 227
changed files holding service Args/Result structs 59

The last row is the one to weigh: this reaches RPC argument and result encoding, not only user-declared structs.

How I would land it

Three options, and I have no stake in which:

  1. As is, in a major, with a release note.
  2. Behind a go: generator option defaulting off, so nobody's wire changes without asking.
  3. Not at all, with the panic fixed in THRIFT-5806: Count zero set fields on a nil union rather than dereferencing it #3822 and the spec deviation documented.

Say which and I will rework it.

Verified: go test ./tests/ -count=1 in lib/go/test passes after regenerating gopath through the Makefile.am recipe. go build ./gopath/src/... reports only the two pre-existing includestest.Numberz errors that #3817 fixes.

CI

The two red cross-test jobs are inherited from master — d6782a87 turned the Python peer-certificate check on and 178 ip-ssl cases fail identically on every open PR, including a two-line .asf.yaml change. See #3818.

This change was created with AI assistance.

@mergeable mergeable Bot added golang Pull requests that update Go code compiler labels Sep 8, 2026
…encing it

Client: go

CountSetFields read the union's fields straight off the receiver, so a
struct holding a nil union field panicked with a nil pointer dereference
the moment it was serialized. Return 0 for a nil receiver, which is the
honest answer and lets the union's own arity check report which union
could not be written.

The panic becomes:

  *Descendant error writing struct: *Descendant write union: exactly one
  field must be set (0 set)

That is a diagnosis rather than a crash, but it is still an error: the
field is not omitted. Omitting it is what the IDL specification asks for
and what Python, Java, Node.js and C# do, and it is deliberately not part
of this change, because the guard that would do it also covers every
default-requiredness struct and exception field and alters the bytes on
the wire for cases that never crashed. That is THRIFT-6204, with the
measurements.

So do not widen this guard to the writer without reading THRIFT-6204
first.
Client: go

Default requiredness means "write if set" (doc/specs/idl.md), and Python,
Java, Node.js and C# all omit such a field when it holds no value. The Go
writer wrote it unconditionally, so a nil pointer field either panicked on
the way out or, when its struct had no members, went onto the wire as an
empty struct.

Guard the write with the generated IsSet helper, as optional fields
already are.

This changes bytes on the wire, and not only for the case that panicked.
A nil field whose struct has no members is written today and is omitted
after this:

  before:  0c 00 01 00 08 00 02 00 00 00 07 00
  after:         08 00 02 00 00 00 07 00

A peer that declares that field required accepts the first and rejects the
second with "Required field E is not set".

is_pointer_field() is true for every struct, union and exception field
whatever its requiredness, and for every cpp.ref field, so the guard is
wider than unions alone. Regenerating every IDL under test/, lib/go/test/
and tutorial/ changes 75 of 650 generated files and guards 227 write
sites, 59 of the changed files holding service Args and Result structs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler golang Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant