feat(appsync): Phase 5 — Management API Coverage + AWS Behavior Alignment + Async Schema Creation#1590
feat(appsync): Phase 5 — Management API Coverage + AWS Behavior Alignment + Async Schema Creation#1590AgustinBertagna wants to merge 21 commits into
Conversation
f20c82b to
38e030e
Compare
…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.
8d359ba to
351ab93
Compare
|
| 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: ... }"
%%{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: ... }"
Reviews (3): Last reviewed commit: "fix(appsync): remove incorrect CME gate ..." | Re-trigger Greptile
| /** 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(); | ||
| } |
There was a problem hiding this comment.
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!
Fixed
Additional gates found during validationWhile validating all 43
Not a bug (Greptile was incorrect)
This is intentional and correct. The AWS AppSync API Reference confirms this:
The Test results438/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.
|
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: One small inconsistency: 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 |
… 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.
|
@hectorvent Thanks for the review. You're right — The fix applied:
All 159 integration tests and 78 SDK tests pass. |
Summary
Implements Phase 5 of the AppSync implementation plan in #1173.
This phase combines two related scopes that close gaps with AWS AppSync behavior:
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.
Async schema creation semantics —
StartSchemaCreationis asynchronous in AWS (returns200 + PROCESSING, transitions toACTIVEorFAILEDvia polling). 43 operations returnConcurrentModificationException 409while a schema creation isPROCESSING.Type of change
fix:)feat:)feat!:orfix!:)What is included
Management API coverage + AWS alignment
service-2.json:BadRequestException400 withreason: "CODE_ERROR"anddetail.codeErrors[]for SDL errors (PARSER_ERROR / VALIDATION_ERROR)ConcurrentModificationException409 for duplicate Source API association (replacesBadRequestException400)span: -1default inCodeErrorLocationgetApiKey,listAllResolvers.AwsException(optionalextendedDatafield) andAwsExceptionMapper.Async schema creation semantics
StartSchemaCreationis now async: the request returns200 + PROCESSING. A newSchemaCreationWorker(singletonExecutorServicewith configurable thread count) compiles the schema in a background thread, transitioning toACTIVEorFAILED(with serialized extended error data in the polledGetSchemaCreationStatus.details).ConcurrentModificationException 409while any schema creation isPROCESSING: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)createGraphqlApi,createApi)StartSchemaCreationon the same APIEmulatorLifecycle.onStartcallsschemaCreationWorker.recoverOrphans()afterstorageFactory.loadAll(), marking any leftoverPROCESSINGasFAILED(e.g., after an emulator crash mid-compilation).New configuration
FLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_THREADSFLOCI_SERVICES_APPSYNC_SCHEMA_WORKER_SHUTDOWN_TIMEOUT_SECONDSTest Results
AppSyncIntegrationTest: 154/154 passAppSyncTest(SDK): 78/78 passEmulatorLifecycleTest: 16/16 passValidation sources
StartSchemaCreationAssociateSourceGraphqlApiConcurrentModificationExceptionJavadocBadRequestExceptionJavadocCodeError/CodeErrorLocationJavadocAppSyncClientJavadoc (operation list)service-2.json(raw)Checklist
./mvnw testpasses locallyservice-2.json