Skip to content

Commit 8bc4f1a

Browse files
committed
Add x-error-responses partial-failure error mask
OpenSearch returns HTTP 200 for partial successes -- bulk item failures, search-shard failures, single-doc replica failures -- so callers must remember a second check after `err == nil`. v4 added an opt-in boolean (Config.ReturnQueryErrors) that converted ALL partial-failure shapes into typed Go errors. That single switch is too coarse: callers who want shard-level errors but tolerate bulk item failures (or vice versa) have no way to express it. Replace the boolean with internal/errmask.ErrorMask, a 15-bit field where each bit corresponds to one wrapper schema in the proposed x-error-responses OpenAPI extension (BulkItems, SearchShards, WriteShards, BroadcastShards, NodeFailures, BulkByScrollFailures, TaskFailures, MultiSearchItems, MultiDocItems, Snapshot{Create,Get}- ShardFailures, SimulateDocFailures, RankEvalFailures, IngestionShardFailures, PitNodeFailures). A set bit MASKS that category; the zero value reports every category. Callers express fine-grained policy in code (Config.Errors = errmask.BulkItems | errmask.SearchShards) or via OPENSEARCH_GO_ERROR_MASK using comma-separated +/- tokens (e.g. "+all,-bulk_items"). Lifecycle (matches OPENSEARCH_GO_ROUTER): v4 (this commit): default `errmask.All` -- preserves pre-bitfield behavior (no partial-failure errors). Config.ReturnQueryErrors=true is honored as a deprecated alias for `errmask.None`. v5: default flips to `errmask.None` (safe by default). v6: Config.Errors / OPENSEARCH_GO_ERROR_MASK removed; behavior is unconditionally `errmask.None`. The hand-written v4 opensearchapi/api_*.go call sites now read c.errors.Has(errmask.<Wrapper>) for each operation's wrapper category. A new hand-written v5preview/opensearchapi/errors.go ports the same typed-error surface (PartialBulkError, PartialSearchError, ShardFailureError, plus the IsPartialFailure / ToleratePartial- Failures / RequireSuccessRate helpers) using v5preview's BulkResponse- Item and ShardSearchFailure types. v5preview Config.Errors and the clientInit(rootClient, mask) signature are wired through both hand-written api.go and the generated clients_gen.go. Spec side: opensearch-openapi.yaml is patched with 15 `_common.errors___<Wrapper>` schemas under components.schemas and 115 operation entries get an x-error-responses annotation. This mirrors the upstream proposal in opensearch-api-specification (see issue-x-partial-failure-mode.md). Once that PR lands and we re-bundle from source, the local patch goes away cleanly. Generator side: cmd/osgen reads x-error-responses from the spec extension into ir.Operation.ErrorWrappers; cmd/osgen/errwrap supplies a hardcoded fallback for plugin operations the spec doesn't yet annotate. The dispatch fragment carries a data-driven `wrappers` map of {Template, Applies}: each wrapper has both an emission template and an Applies predicate that walks the response struct (including embeds via the type registry) to confirm the field path the template references actually exists. This keeps generated code compilable when spec annotations land before the underlying response schema models the relevant field -- v5preview's CreateResp and msearch's union response item are skipped today and will start emitting once those types acquire the missing fields. Ref: opensearch-project#816 Ref: opensearch-project/opensearch-api-specification/pull/1137 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 4046da6 commit 8bc4f1a

27 files changed

Lines changed: 2157 additions & 100 deletions

cmd/osgen/api_extract.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ type apiOperation struct {
5555
QueryParams []apiQueryParam
5656
ResponseRef string // schema key for the 200 response body (e.g. "cluster.health___HealthResponseBody")
5757

58+
// ErrorWrappers lists the partial-failure wrapper-schema names this
59+
// operation may surface alongside its primary 2xx response. Populated
60+
// from the x-error-responses extension on the spec operation.
61+
ErrorWrappers []string
62+
5863
// ResponseSchemaRef is the resolved schema for the 200 response body,
5964
// used to walk inline schemas that aren't in Components.Schemas.
6065
ResponseSchemaRef *openapi3.SchemaRef
@@ -309,6 +314,7 @@ func buildAPIOperation(group string, ops []struct {
309314
DocsURL: docsURL,
310315
ExcludedDistros: extensionStringSlice(op.Extensions, extDistributionsExcluded),
311316
HasBody: op.RequestBody != nil,
317+
ErrorWrappers: errorResponseWrappers(op),
312318
}
313319

314320
// Extract request body schema ref.

cmd/osgen/emit/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func buildOperationFile(dir, pkg, basename string, op *ir.Operation, reg *ir.Typ
226226
}
227227

228228
if len(op.DispatchRoutes) > 0 {
229-
frags = append(frags, &DispatchFragment{Op: op})
229+
frags = append(frags, &DispatchFragment{Op: op, Registry: reg})
230230
}
231231

232232
return &File{

cmd/osgen/emit/frag_clients.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func (f *ClientsFragment) Imports() []Import {
3030
return []Import{
3131
{Path: "github.com/opensearch-project/opensearch-go/v4"},
3232
{Path: "github.com/opensearch-project/opensearch-go/v4/internal/apiutil"},
33+
{Path: "github.com/opensearch-project/opensearch-go/v4/internal/errmask"},
3334
}
3435
}
3536

@@ -120,15 +121,17 @@ var noBody *opensearch.NoBody //nolint:gochecknoglobals // package-internal sent
120121
// Client represents the opensearchapi Client summarizing all API calls.
121122
type Client struct {
122123
Client *opensearch.Client
124+
errors errmask.ErrorMask
123125
{{- range .TopLevel}}
124126
{{.FieldName}} {{.TypeName}}
125127
{{- end}}
126128
}
127129
128130
// clientInit initializes a Client with all sub-clients.
129-
func clientInit(rootClient *opensearch.Client) *Client {
131+
func clientInit(rootClient *opensearch.Client, mask errmask.ErrorMask) *Client {
130132
client := &Client{
131133
Client: rootClient,
134+
errors: mask,
132135
}
133136
{{- range .InitStmts}}
134137
{{.}}

cmd/osgen/emit/frag_clients_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ func TestClientsFragment_Body(t *testing.T) {
3636
}{
3737
{name: "Client struct", want: "type Client struct"},
3838
{name: "Client field", want: "Client *opensearch.Client"},
39+
{name: "errors mask field", want: "errors errmask.ErrorMask"},
3940
{name: "top-level Cat", want: "Cat catClient"},
4041
{name: "top-level Indices", want: "Indices indicesClient"},
41-
{name: "clientInit", want: "func clientInit(rootClient *opensearch.Client) *Client"},
42+
{name: "clientInit", want: "func clientInit(rootClient *opensearch.Client, mask errmask.ErrorMask) *Client"},
43+
{name: "errors init", want: "errors: mask,"},
4244
{name: "init Cat", want: "client.Cat = catClient{apiClient: client}"},
4345
{name: "init Indices", want: "client.Indices = indicesClient{apiClient: client}"},
4446
{name: "nested init Alias", want: "client.Indices.Alias = aliasClient{apiClient: client}"},
@@ -70,7 +72,7 @@ func TestClientsFragment_Imports(t *testing.T) {
7072
}}
7173

7274
imps := frag.Imports()
73-
require.Len(t, imps, 2)
75+
require.Len(t, imps, 3)
7476
}
7577

7678
func TestNewClientsFile_Render(t *testing.T) {
@@ -92,6 +94,7 @@ func TestNewClientsFile_Render(t *testing.T) {
9294
require.Contains(t, output, "package "+ir.DefaultCorePkgName)
9395
require.Contains(t, output, `"github.com/opensearch-project/opensearch-go/v4"`)
9496
require.Contains(t, output, `"github.com/opensearch-project/opensearch-go/v4/internal/apiutil"`)
97+
require.Contains(t, output, `"github.com/opensearch-project/opensearch-go/v4/internal/errmask"`)
9598
}
9699

97100
func TestNewClientsFile_NilWhenEmpty(t *testing.T) {

0 commit comments

Comments
 (0)