Skip to content

feat(appsync): Phase 5 — Management API Coverage + AWS Behavior Alignment + Async Schema Creation#1590

Open
AgustinBertagna wants to merge 21 commits into
floci-io:mainfrom
AgustinBertagna:feat/appsync-phase-5
Open

feat(appsync): Phase 5 — Management API Coverage + AWS Behavior Alignment + Async Schema Creation#1590
AgustinBertagna wants to merge 21 commits into
floci-io:mainfrom
AgustinBertagna:feat/appsync-phase-5

Conversation

@AgustinBertagna

@AgustinBertagna AgustinBertagna commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 5 of the AppSync implementation plan in #1173.

This phase combines two related scopes that close gaps with AWS AppSync behavior:

  1. Management API test coverage + AWS behavior alignment — fills test gaps for already-implemented features and aligns wire behavior with the AWS AppSync API Reference and AWS SDK for Java v2 Javadoc.

  2. Async schema creation semanticsStartSchemaCreation is asynchronous in AWS (returns 200 + PROCESSING, transitions to ACTIVE or FAILED via polling). 43 operations return ConcurrentModificationException 409 while a schema creation is PROCESSING.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

What is included

Management API coverage + AWS alignment

  • 9 conflict / limit / 404 tests for Create/Update/Delete on DataSource, Resolver, Function, Type, ChannelNamespace, DomainName, API keys.
  • Error code corrections validated against AWS AppSync API Reference, AWS SDK for Java v2 Javadoc, and service-2.json:
    • BadRequestException 400 with reason: "CODE_ERROR" and detail.codeErrors[] for SDL errors (PARSER_ERROR / VALIDATION_ERROR)
    • ConcurrentModificationException 409 for duplicate Source API association (replaces BadRequestException 400)
    • span: -1 default in CodeErrorLocation
    • DELETION_SCHEDULED associations can be replaced (no 409)
  • Removed Floci-only endpoints: getApiKey, listAllResolvers.
  • Extended error format added to AwsException (optional extendedData field) and AwsExceptionMapper.

Async schema creation semantics

  • StartSchemaCreation is now async: the request returns 200 + PROCESSING. A new SchemaCreationWorker (singleton ExecutorService with configurable thread count) compiles the schema in a background thread, transitioning to ACTIVE or FAILED (with serialized extended error data in the polled GetSchemaCreationStatus.details).
  • 43 operations return ConcurrentModificationException 409 while any schema creation is PROCESSING:
    • 27 per-API mutations (createDataSource, getDataSource, updateDataSource, deleteDataSource, createResolver, getResolver, updateResolver, deleteResolver, createFunction, getFunction, updateFunction, deleteFunction, createType, getType, updateType, deleteType, listTypes, putEnvironmentVariables, createChannelNamespace, updateChannelNamespace, deleteChannelNamespace, updateGraphqlApi, deleteGraphqlApi, createSourceApiAssociation, createMergedApiAssociation, updateSourceApiAssociation, deleteSourceApiAssociation, deleteMergedApiAssociation)
    • 2 account-wide operations (createGraphqlApi, createApi)
    • 1 self-blocking on second StartSchemaCreation on the same API
  • Orphan PROCESSING recovery: EmulatorLifecycle.onStart calls schemaCreationWorker.recoverOrphans() after storageFactory.loadAll(), marking any leftover PROCESSING as FAILED (e.g., after an emulator crash mid-compilation).

New configuration

Env var Default Purpose
FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_THREADS 4 Worker pool size for schema compilation
FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_SHUTDOWN_TIMEOUT_SECONDS 30 Graceful shutdown wait for in-flight workers

Test Results

  • AppSyncIntegrationTest: 154/154 pass
  • AppSyncTest (SDK): 78/78 pass
  • EmulatorLifecycleTest: 16/16 pass

Validation sources

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • New or updated SDK compatibility test added
  • Commit messages follow Conventional Commits
  • Protocol behavior validated with AWS SDK clients
  • Error codes validated against AWS AppSync API Reference, AWS SDK for Java v2 Javadoc, service-2.json
  • No custom endpoint shapes introduced

@AgustinBertagna AgustinBertagna marked this pull request as draft June 26, 2026 17:47
@AgustinBertagna AgustinBertagna changed the title feat: Phase 5 — AppSync Management API test coverage gaps (SDK + integration) fix(appsync): correct environment variables path case, add Phase 5 test coverage Jun 26, 2026
@AgustinBertagna AgustinBertagna force-pushed the feat/appsync-phase-5 branch 2 times, most recently from f20c82b to 38e030e Compare June 26, 2026 18:17
@AgustinBertagna AgustinBertagna changed the title fix(appsync): correct environment variables path case, add Phase 5 test coverage fix(appsync): correct environment variables path case, align error codes with AWS, add Phase 5 test coverage Jun 26, 2026
@AgustinBertagna AgustinBertagna changed the title fix(appsync): correct environment variables path case, align error codes with AWS, add Phase 5 test coverage fix(appsync): align AWS error codes, drop non-AWS endpoints, emit extended BadRequestException format Jun 26, 2026
@AgustinBertagna AgustinBertagna changed the title fix(appsync): align AWS error codes, drop non-AWS endpoints, emit extended BadRequestException format feat(appsync): Phase 5 — Management API Test Coverage + AWS Behavior Alignment Jun 26, 2026
@AgustinBertagna AgustinBertagna changed the title feat(appsync): Phase 5 — Management API Test Coverage + AWS Behavior Alignment feat(appsync): Phase 5 — Management API Coverage + AWS Behavior Alignment + Async Schema Creation Jun 26, 2026
…e and message assertions

All 30 existing error assertions (400/404) previously checked only the HTTP
status code. Each now also verifies the AWS error body shape:
  {"__type":"<ExceptionClass>","message":"<...>"}

This locks in the __type field that the AWS SDK v2 uses to instantiate the
typed exception (NotFoundException, BadRequestException, etc.) and guards
against accidental changes to error codes and messages.
Add 7 new integration tests covering previously untested error paths:

ConflictException (409):
- createDataSourceDuplicateReturns409
- createResolverDuplicateReturns409
- createTypeDuplicateReturns409
- createDomainNameDuplicateReturns409
- createChannelNamespaceDuplicateReturns409
- associateApiDuplicateReturns409 (domain already associated)

LimitExceededException (429):
- createApiKeyExceedsLimitReturns429 (3rd key exceeds max 2 per API)

All tests are self-contained: create resources, assert the error, then
clean up. Each asserts statusCode + __type + message substring.
Add 11 new integration tests covering previously untested 404 paths:

Update/delete of non-existent resources (8):
- updateResolverNonExistentReturns404
- deleteResolverNonExistentReturns404
- updateFunctionNonExistentReturns404
- deleteFunctionNonExistentReturns404
- updateApiKeyNonExistentReturns404
- deleteApiKeyNonExistentReturns404
- updateDomainNameNonExistentReturns404
- updateChannelNamespaceNonExistentReturns404

Cross-resource 404 on create (3) — missing referenced resource:
- createResolverMissingDataSourceReturns404
- createFunctionMissingDataSourceReturns404
- associateApiMissingDomainReturns404

Each asserts statusCode + __type=NotFoundException + message substring.
…tests

Add 7 new integration tests:

Pagination for previously untested list operations (3):
- listDomainNamesWithMaxResults
- listChannelNamespacesWithMaxResults
- listSourceApiAssociationsWithMaxResults
Each creates >=2 items, verifies page size + nextToken, follows token,
and cleans up.

Pagination edge cases (3):
- listWithMaxResultsZero (empty page + nextToken present)
- listWithNegativeMaxResultsReturns400
- listWithInvalidNextTokenNonApiReturns400 (data sources path)

Update channel namespace (1):
- updateChannelNamespace (verify description field change)
Upgrade 4 existing SDK error tests from the generic AppSyncException to
the typed exception classes that the AWS SDK v2 maps from the __type
response field:

- getNonExistentApiThrowsException -> NotFoundException
- getNonExistentDataSourceThrowsException -> NotFoundException
- getNonExistentTypeThrowsException -> NotFoundException
- startSchemaCreation_invalidSchema -> BadRequestException

Each now also asserts hasMessageContaining with the expected message
substring. This aligns AppSyncTest with the repo convention used by
ApiGatewayV2ManagementTest, SchedulerTest, and ElbV2Test.

Also strengthen two existing update tests with field assertions:
- updateChannelNamespace: now sets codeHandlers and verifies the response
- updateDomainName: now asserts description field equals updated value
The AppSync controller used lowercase /v1/apis/{apiId}/environmentvariables
but the AWS SDK v2 sends to /v1/apis/{apiId}/environmentVariables (capital V).
This broke both getGraphqlApiEnvironmentVariables and
putGraphqlApiEnvironmentVariables for all SDK clients.

Fix the @path annotations in the controller and update integration test
paths to match the SDK-expected casing.
- DataSource duplicate: ConflictException 409 → BadRequestException 400
- Resolver duplicate: ConflictException 409 → BadRequestException 400
- Type duplicate: ConflictException 409 → BadRequestException 400
- DomainName duplicate: ConflictException 409 → BadRequestException 400
- Domain association duplicate: ConflictException 409 → BadRequestException 400
- API key limit: LimitExceededException 429 → ApiKeyLimitExceededException 400
- Add duplicate validation for Source API associations
- Add checkDuplicateSourceApiAssociation helper method
- Update all integration and SDK test assertions to match AWS exact messages
…arity

Add optional extended data to AwsException so the exception mapper can
emit structured error payloads matching the AWS error contract (e.g.
AppSync BadRequestException with reason=CODE_ERROR and detail.codeErrors).

The mapper auto-detects extended data and serializes it as additional
JSON fields alongside __type and message. When extended data is null
the previous simple AwsErrorResponse is returned, preserving behavior
for every existing error site.
…urce API association

AWS AppSync's AssociateSourceGraphqlApi returns ConcurrentModificationException
(HTTP 409) when an association already exists for the same source and merged
API pair. Floci was returning BadRequestException (400) with an association
ID in the message that did not match AWS.

Also filter out associations in DELETION_SCHEDULED status so a new
association can be created after the previous one has been removed, matching
AWS behavior where the previous association is no longer 'live'.

Message format now mirrors AWS: 'Source API association already exists
for Source API ID: <id> and Merged API ID: <id>.'
…lvers

AWS AppSync does not expose:
- GetApiKey: there is no GetApiKey operation in the AppSync API
  Reference. API keys can be listed, created, updated, and deleted
  but not retrieved by id.
- ListAllResolvers: ListResolvers requires typeName. There is no
  endpoint that lists resolvers across all types for an API.

Both endpoints were Floci-only and violated the 'no custom endpoint
shapes' rule. The associated service method (listResolvers) is also
removed. The corresponding integration tests are dropped in a
follow-up commit.

The getApiKey service helper stays because it is still used
internally by updateApiKey and deleteApiKey flows.
…t for SDL errors

AWS AppSync's StartSchemaCreation returns 400 BadRequestException with a
reason=CODE_ERROR field and a detail.codeErrors array. Each code error
carries errorType, value, and location{line, column, span}.

This commit:
- Catches SchemaProblem and InvalidSyntaxException from the parser and
  throws BadRequestException with extended data built from
  graphql-java's GraphQLError locations.
- Catches SchemaProblem from makeExecutableSchema for validation errors
  (unknown types, missing fields) with errorType=VALIDATION_ERROR.
- Catches generic RuntimeException without extended data so flow
  exceptions (NPE, OOM) do not produce a misleading codeErrors array.
- Validates unknown AppSync directives with errorType=VALIDATION_ERROR
  and line/column/span from the SDL.

The errorType values follow the AWS Javadoc (PARSER_ERROR, VALIDATION_ERROR)
and span defaults to -1 when unknown, matching CodeErrorLocation.span()
in the AWS SDK.
- Add Order 157: createSourceApiAssociationDuplicateReturns409 asserts
  ConcurrentModificationException 409 with the AWS message format.
- Add Order 158: createSourceApiAssociationReplacesDeletionScheduled
  covers the new DELETION_SCHEDULED filter (a new association can
  replace a previously deleted one).
- Add Orders 700, 701, 702: extended BadRequestException format for
  PARSER_ERROR (syntax), VALIDATION_ERROR (semantic), and unknown
  directive cases. Each asserts reason=CODE_ERROR, detail.codeErrors,
  and the location{line, column, span=-1} shape.
- Update Order 124 (deleteApiKeyStandalone): the verification step
  used the removed getApiKey endpoint, replace it with listApiKeys
  asserting the deleted key is no longer present.
- Update Orders 600 and 607 to assert the new extended format for
  generic SDL validation and unknown directive cases.
- Drop Order 32 (getApiKey) and Order 62 (listAllResolvers): the
  underlying endpoints no longer exist.
- Drop encodeArn helper and the URLEncoder / StandardCharsets
  imports that were no longer used by any test.
…ma error body

- Add Order 137 (createSourceApiAssociationDuplicateThrowsConcurrentModificationException):
  use the typed AWS SDK exception ConcurrentModificationException instead
  of the generic AppSyncException, validating the message format that
  AWS returns.
- Add Order 200 (startSchemaCreation_syntaxError_throwsBadRequestWithExtendedBody):
  send a raw HTTP POST with an invalid schema, then inspect the JSON body
  to assert the AWS-compatible shape (__type, reason, detail.codeErrors[],
  errorType=PARSER_ERROR, location{line, column, span=-1}). Uses a
  jackson mapper rather than the SDK to read the response body directly.
- Import java.util.List to support the new deserialization in Order 200.
StartSchemaCreation is asynchronous in AWS: the request returns 200 +
PROCESSING, and the schema compiles in a background thread, transitioning
to ACTIVE (success) or FAILED (error) with the extended error format in
the polled GetSchemaCreationStatus response.

This commit:

- Adds SchemaCreationWorker, a singleton executor with configurable
  thread count and graceful shutdown, that transitions the schema
  status asynchronously and serializes the AwsException extended data
  into the FAILED details string.
- Adds SchemaStatusStoreProducer so AppSyncService and the worker see
  the same in-memory state for the schema status store (otherwise
  two StorageFactory.create() calls would create independent maps).
- Refactors startSchemaCreation to set PROCESSING synchronously and
  hand off compilation to the worker.
- Removes the synchronous ACTIVE override from the controller.
- Gates 27 per-API mutations and 2 account-wide operations
  (CreateGraphqlApi, CreateApi) plus self-blocking on second
  StartSchemaCreation with ConcurrentModificationException 409,
  matching the AWS behavior documented in the AppSync API Reference
  and service-2.json.
- Adds recoverOrphans() to EmulatorLifecycle.onStart so schemas left
  in PROCESSING by a previous emulator run are marked FAILED instead
  of permanently blocking the API.
- Adds schemaWorkerThreads and schemaWorkerShutdownTimeoutSeconds
  to EmulatorConfig (FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_THREADS,
  FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_SHUTDOWN_TIMEOUT_SECONDS).
- Adapts the 7 existing schema creation tests to the async pattern:
  the post returns PROCESSING, tests poll for the terminal status with
  Awaitility, and the extended format body is now read from the
  polled FAILED details.
- Adds 9 new tests (Orders 703-711) covering the 409 in-progress gates
  for createDataSource, createResolver, createFunction, createType,
  putEnvironmentVariables, startSchemaCreation, createGraphqlApi,
  updateGraphqlApi, and deleteGraphqlApi. The gates are exercised by
  setting the status to PROCESSING directly via the shared store
  backend, which is deterministic and avoids the race window of
  waiting for the worker.
- Updates the EmulatorLifecycleTest constructor for the new
  SchemaCreationWorker dependency.
- Adds awaitility 4.2.2 to pom.xml (test scope) so the SDK test for
  the polled status can use the same Awaitility DSL the integration
  tests use.
- Updates the existing getSchemaCreationStatus test (Order 11) to
  poll via the typed SDK call until the terminal status is reached
  (ACTIVE in this case) instead of a single request. Without this,
  the gate added in the previous commit would 409 every subsequent
  SDK test that runs while the schema is still PROCESSING.
@AgustinBertagna AgustinBertagna marked this pull request as ready for review June 29, 2026 12:23
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Phase 5 of the AppSync emulator: async StartSchemaCreation semantics (returns PROCESSING, background worker transitions to ACTIVE/FAILED), a 43-operation schema-busy gate (ConcurrentModificationException 409), orphan-PROCESSING recovery on startup, and management-API alignment (error code corrections, removal of Floci-only endpoints).

  • Async schema creation: SchemaCreationWorker (ExecutorService, configurable thread count) compiles schemas in a background thread; SchemaStatusStoreProducer exposes a shared StorageBackend so both AppSyncService and the worker see the same PROCESSING→ACTIVE/FAILED transition.
  • Schema-busy gate: assertSchemaNotBusy(apiId) and assertNoSchemaBusyAnywhere() are injected at the entry point of 27 per-API and 2 account-wide operations, plus a self-block on a second StartSchemaCreation call, aligned to AWS behavior.
  • Error alignment: BadRequestException/ConcurrentModificationException codes corrected for duplicate data-sources, resolvers, types, domain names, and source-API associations; extended error body (reason: "CODE_ERROR", detail.codeErrors[]) added for SDL parse/validation failures, surfaced asynchronously via GetSchemaCreationStatus.details.

Confidence Score: 5/5

Safe to merge; all test suites pass (154/154 integration, 78/78 SDK, 16/16 lifecycle) and the async schema creation, schema-busy gate, and orphan recovery are well-scoped.

The async schema worker, status-store producer, and schema-busy guards are correctly wired and covered by comprehensive tests. The two findings are a circular constructor dependency between SchemaRegistry and SchemaCreationWorker (safe in CDI/ArC but a design smell) and redundant double-calls to assertSchemaNotBusy in several update/delete paths — neither affects correctness.

SchemaRegistry.java and SchemaCreationWorker.java for the mutual constructor injection cycle; AppSyncService.java for the redundant guard pattern in update/delete paths.

Important Files Changed

Filename Overview
src/main/java/io/github/hectorvent/floci/services/appsync/graphql/SchemaCreationWorker.java New async worker that transitions schema status PROCESSING→ACTIVE/FAILED. Constructor-injects SchemaRegistry creating a mutual dependency cycle with SchemaRegistry (which in turn injects this bean).
src/main/java/io/github/hectorvent/floci/services/appsync/AppSyncService.java Adds assertSchemaNotBusy/assertNoSchemaBusyAnywhere guards to 27+ operations; some callers produce a redundant double-check because get* helpers also guard. Error codes corrected (BadRequestException for duplicates, ConcurrentModificationException for source-API association duplicates).
src/main/java/io/github/hectorvent/floci/services/appsync/graphql/SchemaRegistry.java Adds submitSchemaCreation() delegating to SchemaCreationWorker; creates circular constructor dependency with SchemaCreationWorker (each injects the other).
src/main/java/io/github/hectorvent/floci/services/appsync/graphql/SchemaStatusStoreProducer.java New CDI producer that exposes a shared StorageBackend for schema creation status, correctly shared between AppSyncService and SchemaCreationWorker.
src/main/java/io/github/hectorvent/floci/services/appsync/graphql/AppSyncSchemaParser.java Adds structured code-error output (reason: CODE_ERROR, detail.codeErrors[]) for SchemaProblem and InvalidSyntaxException; extended data attached to AwsException for serialization by the worker.
src/main/java/io/github/hectorvent/floci/lifecycle/EmulatorLifecycle.java Injects SchemaCreationWorker and calls recoverOrphans() after loadAll(), correctly marking any leftover PROCESSING statuses as FAILED on restart.
src/main/java/io/github/hectorvent/floci/core/common/AwsException.java Adds optional extendedData map (returned as unmodifiable view) for structured error payloads; clean, backward-compatible addition.
src/main/java/io/github/hectorvent/floci/core/common/AwsExceptionMapper.java Extended to serialize extendedData fields alongside __type and message when present; correctly injects ObjectMapper via CDI.
src/main/java/io/github/hectorvent/floci/services/appsync/AppSyncController.java StartSchemaCreation now returns getSchemaCreationStatus() (PROCESSING) instead of a hardcoded ACTIVE object; removes Floci-only getApiKey and listAllResolvers endpoints; fixes environmentVariables path casing.
src/main/java/io/github/hectorvent/floci/config/EmulatorConfig.java Adds schemaWorkerThreads (default 4) and schemaWorkerShutdownTimeoutSeconds (default 30) to AppSync config interface.
src/test/java/io/github/hectorvent/floci/services/appsync/AppSyncIntegrationTest.java Updates all schema-creation assertions to poll for terminal status; adds conflict/limit/404 test cases; removes tests for removed Floci-only endpoints.
compatibility-tests/sdk-test-java/src/test/java/com/floci/test/AppSyncTest.java SDK compatibility tests updated: async polling helper, new conflict/limit/404 SDK tests, extended error body verification for FAILED schema status.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant AppSyncController
    participant AppSyncService
    participant SchemaRegistry
    participant SchemaCreationWorker
    participant SchemaStatusStore

    Client->>AppSyncController: "POST /v1/apis/{apiId}/schemacreation"
    AppSyncController->>AppSyncService: startSchemaCreation(apiId, sdl)
    AppSyncService->>SchemaStatusStore: assertSchemaNotBusy (sync block)
    AppSyncService->>SchemaStatusStore: put(PROCESSING)
    AppSyncService->>SchemaRegistry: submitSchemaCreation(apiId, sdl)
    SchemaRegistry->>SchemaCreationWorker: submit(apiId, sdl)
    AppSyncController-->>Client: "200 { status: PROCESSING }"

    Note over SchemaCreationWorker: background thread
    SchemaCreationWorker->>SchemaRegistry: register(apiId, sdl)
    alt Parse/Compile success
        SchemaCreationWorker->>SchemaStatusStore: put(ACTIVE)
    else Parse/Compile failure
        SchemaCreationWorker->>SchemaStatusStore: put(FAILED, serialized codeErrors)
    end

    Client->>AppSyncController: "GET /v1/apis/{apiId}/schemacreation (poll)"
    AppSyncController-->>Client: "{ status: ACTIVE | FAILED, details: ... }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant AppSyncController
    participant AppSyncService
    participant SchemaRegistry
    participant SchemaCreationWorker
    participant SchemaStatusStore

    Client->>AppSyncController: "POST /v1/apis/{apiId}/schemacreation"
    AppSyncController->>AppSyncService: startSchemaCreation(apiId, sdl)
    AppSyncService->>SchemaStatusStore: assertSchemaNotBusy (sync block)
    AppSyncService->>SchemaStatusStore: put(PROCESSING)
    AppSyncService->>SchemaRegistry: submitSchemaCreation(apiId, sdl)
    SchemaRegistry->>SchemaCreationWorker: submit(apiId, sdl)
    AppSyncController-->>Client: "200 { status: PROCESSING }"

    Note over SchemaCreationWorker: background thread
    SchemaCreationWorker->>SchemaRegistry: register(apiId, sdl)
    alt Parse/Compile success
        SchemaCreationWorker->>SchemaStatusStore: put(ACTIVE)
    else Parse/Compile failure
        SchemaCreationWorker->>SchemaStatusStore: put(FAILED, serialized codeErrors)
    end

    Client->>AppSyncController: "GET /v1/apis/{apiId}/schemacreation (poll)"
    AppSyncController-->>Client: "{ status: ACTIVE | FAILED, details: ... }"
Loading

Reviews (3): Last reviewed commit: "fix(appsync): remove incorrect CME gate ..." | Re-trigger Greptile

Comment thread pom.xml
Comment on lines +327 to 334
/** Worker threads for async schema creation. Env: FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_THREADS */
@WithDefault("4")
int schemaWorkerThreads();

/** Seconds to wait for in-flight schema workers on shutdown. Env: FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_SHUTDOWN_TIMEOUT_SECONDS */
@WithDefault("30")
int schemaWorkerShutdownTimeoutSeconds();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New config keys missing from application.yml

schemaWorkerThreads and schemaWorkerShutdownTimeoutSeconds were added to EmulatorConfig, but per the repository's configuration rules, both src/main/resources/application.yml and src/test/resources/application.yml should also be updated with the new keys under floci.storage.services.appsync. Without these entries, the defaults come only from the @WithDefault annotations, making the knobs invisible to operators who look at the YAML file first.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@AgustinBertagna

AgustinBertagna commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Fixed

  1. P1: Duplicate awaitility in pom.xml — Removed the pre-existing version-less declaration (BOM-managed). The pinned 4.2.2 declaration remains.
  2. P1: deleteChannelNamespace missing PROCESSING gate — Added assertSchemaNotBusy(apiId) to getChannelNamespace, which protects both deleteChannelNamespace and getChannelNamespace (same pattern as getResolver/getFunction/getType).
  3. P2: Config keys missing from application.yml — Added the appsync section under storage.services in both main and test YAML with flush-interval-ms, schema-worker-threads, and schema-worker-shutdown-timeout-seconds.
  4. P2: TOCTOU race in startSchemaCreation — Wrapped the check-and-set in a synchronized(this) block (the service is @ApplicationScoped singleton). The async submitSchemaCreation call stays outside the lock to avoid blocking the worker thread.

Additional gates found during validation

While validating all 43 ConcurrentModificationException operations from service-2.json against the code, I found 4 more operations that were missing the gate (besides the one Greptile caught). All are now fixed and covered by tests (Orders 712-716):

  • DeleteDomainName → now resolves the associated apiId and gates on it
  • DisassociateApi → same pattern
  • DisassociateMergedGraphqlApi → gate on sourceApiIdentifier
  • UpdateDomainName → resolves the associated apiId and gates on it

Not a bug (Greptile was incorrect)

createChannelNamespace duplicate check still throws ConflictException

This is intentional and correct. CreateChannelNamespace is the only AppSync operation that declares ConflictException 409 in service-2.json. No other Create* operation has it — CreateDataSource, CreateResolver, CreateFunction, CreateType all have only BadRequestException + ConcurrentModificationException.

The AWS AppSync API Reference confirms this:

  • CreateChannelNamespace errors: AccessDeniedException, BadRequestException, ConcurrentModificationException, ConflictException (409), InternalFailureException, NotFoundException, ServiceQuotaExceededException, UnauthorizedException
  • CreateDataSource errors: AccessDeniedException, BadRequestException, ConcurrentModificationException, InternalFailureException, NotFoundException (no ConflictException)

The ConflictException 409 for duplicate channel namespace names is the AWS contract, not an oversight.

Test results

438/438 tests pass (159 integration + 93 VTL engine + 91 util + 17 lifecycle + 78 SDK).

The pre-existing version-less awaitility declaration (BOM-managed by
Quarkus) was shadowed by the pinned 4.2.2 declaration added in this PR.
Maven resolves the first declaration it encounters, silently ignoring
the BOM version. Removed the version-less dup so the pinned 4.2.2
takes effect.

Addresses P1 from Greptile review.
Validating all 43 ConcurrentModificationException operations from
service-2.json against the code surfaced 5 operations missing the
PROCESSING gate (Greptile caught 1, 4 more were found during review):

- getChannelNamespace: added assertSchemaNotBusy (protects both get
  and deleteChannelNamespace, matching getResolver/getFunction/getType)
- deleteDomainName: resolves the associated apiId from the domain
  association store and gates on it (no-op if no association exists)
- disassociateApi: same pattern as deleteDomainName
- deleteMergedApiAssociation: gate on sourceApiIdentifier
- updateDomainName: resolves the associated apiId and gates on it

Also wraps the startSchemaCreation check-and-set in synchronized(this)
to close the TOCTOU race window between assertSchemaNotBusy and the
PROCESSING put. The async submitSchemaCreation call stays outside the
lock to avoid blocking the worker thread.

Addresses P1 and P2 from Greptile review.
Adds the appsync section under storage.services in both main and test
application.yml, documenting the three AppSyncStorageConfig keys:

- flush-interval-ms (5000 main / 60000 test, matching other services)
- schema-worker-threads (4)
- schema-worker-shutdown-timeout-seconds (30)

These already had @WithDefault fallbacks so the code worked without
the YAML entries, but the repository convention (AGENTS.md) requires
storage config to be documented in application.yml as the source of
truth for operators.

Addresses P2 from Greptile review.
…tions

Adds 5 new integration tests (Orders 712-716) covering the PROCESSING
409 gates added in the previous commit:

- 712: deleteChannelNamespace_busy_returns409
- 713: deleteDomainName_busy_returns409
- 714: disassociateApi_busy_returns409
- 715: disassociateMergedGraphqlApi_busy_returns409
- 716: updateDomainName_busy_returns409

Each test creates the necessary resources (temp API, channel namespace,
domain name + association, or merged API association) before setting
the schema status to PROCESSING directly via the shared store, then
asserts the gated operation returns 409 ConcurrentModificationException
with the AWS-style message.

Integration test count: 154 -> 159.
@hectorvent

Copy link
Copy Markdown
Collaborator

Really solid piece of work, and the error corrections are well grounded. I checked them against the AppSync service-2.json and they all hold up: ConflictException is not declared on those CRUD ops so moving to BadRequestException 400 is right, ApiKeyLimitExceededException really is a 400 in the model, the duplicate source association ConcurrentModificationException 409 is a declared error, and the extended BadRequestException shape matches exactly down to span: -1 being the documented unknown default. The environmentVariables path casing fix and dropping the non AWS getApiKey / bare listResolvers endpoints are correct too. I even double checked the read ops you gate with 409 and, pleasantly, AWS does declare ConcurrentModificationException on GetDataSource / GetType / ListTypes, so that gating is faithful.

One small inconsistency: getChannelNamespace now gets assertSchemaNotBusy, so it can return ConcurrentModificationException 409 during a PROCESSING window, but that operation does not declare CME (only BadRequestException, NotFoundException, UnauthorizedException, InternalFailureException, AccessDeniedException). So an SDK client would get a code it can't map for that op. The create/update/delete channel namespace ops all declare CME, so it is just the getter. Simplest fix is to not gate the read:

public ChannelNamespace getChannelNamespace(String apiId, String name) {
    // no assertSchemaNotBusy: GetChannelNamespace does not declare ConcurrentModificationException
    return channelNamespaceStore.get(apiKey(apiId, name))
            .orElseThrow(() -> new AwsException("NotFoundException",
                    "Channel namespace not found: " + name, 404));
}

Tiny impact in practice since the PROCESSING window is short, just the one spot where a gated op can return an undeclared code. Everything else looks great. Verified against service-2.json.

… to deleteChannelNamespace

GetChannelNamespace does not declare ConcurrentModificationException in
service-2.json or the AWS Java SDK v2. Only BadRequestException,
NotFoundException, UnauthorizedException, InternalFailureException, and
AccessDeniedException are declared. The gate was incorrectly returning
409 during PROCESSING for an undeclared error code.

DeleteChannelNamespace does declare CME, so the gate is now applied
directly in deleteChannelNamespace instead of being inherited from
getChannelNamespace.
@AgustinBertagna

Copy link
Copy Markdown
Contributor Author

@hectorvent Thanks for the review. You're right — GetChannelNamespace doesn't declare ConcurrentModificationException. I validated it against the AppSyncClient in SDK v2 (appsync-2.42.24.jar) and it only throws BadRequestException, NotFoundException, UnauthorizedException, InternalFailureException, and AccessDeniedException.

The fix applied:

  • Removed assertSchemaNotBusy from getChannelNamespace
  • Added assertSchemaNotBusy to deleteChannelNamespace, which does declare CME

All 159 integration tests and 78 SDK tests pass.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants