Skip to content

Add AWS MemoryDB Full-Parity Support - #305

Merged
thzgajendra merged 6 commits into
stackshy:developmentfrom
thzgajendra:feat/memorydb-full-parity
Jul 31, 2026
Merged

Add AWS MemoryDB Full-Parity Support#305
thzgajendra merged 6 commits into
stackshy:developmentfrom
thzgajendra:feat/memorydb-full-parity

Conversation

@thzgajendra

@thzgajendra thzgajendra commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Objective

Add full-parity support for AWS MemoryDB for Redis/Valkey — the next missing managed database-server surface after AlloyDB (#304). Covers every control-plane resource, connection, child resource, metric, cost dimension, pagination, and fleet-maintenance operation, so a real aws-sdk-go-v2/service/memorydb client works unchanged against a custom endpoint.

What we found

  • MemoryDB is a control-plane-only service (durable Redis/Valkey clusters, no Set/Get data plane), so it does not fit services/cache/driver. It gets its own driver.
  • Broad resource graph: clusters (shards → nodes → endpoints), ACLs & users, parameter groups, subnet groups, snapshots, tags, engine-version/event catalogs, multi-region clusters, reserved nodes, and service updates.

How we fixed it

  • Driver (services/memorydb/driver): 33 core operations + three type-asserted optional capabilities — MultiRegion (7), ReservedNodes (3), ServiceUpdates (2).
  • Provider (providers/aws/memorydb): in-memory Mock with shard/node/endpoint topology, reference validation, restore-from-snapshot (full shape incl. replicas/subnet/param/TLS), shard failover, multi-region parent linkage (members registered on create, guarded on delete), CloudWatch metrics, and clone-on-read on every return path.
  • Server (server/aws/memorydb): AWS JSON 1.1 handler on the AmazonMemoryDB. target prefix; canonical errors map to typed faults; pagination (MaxResults/NextToken) on every Describe* op via an opaque base64 offset token over the deterministic result set.
  • Wiring: registered in the AWS provider/server bundles; dedicated memorydb:* cost keys; docs/services.md section + counts.

Review round (thanks @NitinKumar004)

Medium: multi-region parent linkage now live; reserved-node duplicate purchase → ReservedNodeAlreadyExistsFault.
Low: restore preserves replica count (+subnet/param/TLS); UpdateCluster dangling refs → InvalidArgument; FailoverShard checks the requested shard; DeleteCluster rejects a duplicate final snapshot; CreateACL de-dupes users; MR param-group noun fixed; ListTags deterministic; doc typos fixed.
Extras (opted in): pagination across all Describe* ops; service updates (DescribeServiceUpdates + BatchUpdateCluster with partial-success UnprocessedClusters).

Alternatives not taken

  • Reusing the cache driver: rejected — MemoryDB has no data plane.
  • Emitting nullable timestamps (node CreateTime, reserved-node StartTime, event Date, service-update dates): AWS JSON 1.1 encodes timestamps as epoch numbers, which encoding/json can't produce for a time.Time; those nullable fields are omitted so SDK deserialization stays clean. All other fields round-trip through the real SDK.
  • Driver-level pagination: rejected in favor of server-side paging over the deterministic result set — keeps the driver interface clean and the token stable.

Docs / Test / Playground

  • docs/services.md: MemoryDB section (per-resource tables incl. Service Updates + a pagination note) + recomputed totals (Grand Total 1310, +137 optional).
  • Tests: provider unit tests (topology, ref validation, failover, ACL/user linkage + dedupe, param/subnet groups, snapshot/restore incl. replica/TLS fidelity, multi-region parent linkage, reserved nodes, service updates, clone-on-read aliasing, metrics, tags/catalogs) + server SDK round-trip tests via the real client (clusters, ACLs/users, param/subnet groups, snapshots, multi-region, reserved nodes, pagination, service updates, typed wire-error assertions incl. ReservedNodeAlreadyExistsFault). Cost rate-catalog test.

Test plan

  • go build ./..., go vet ./...
  • go test ./... (full module, green)
  • golangci-lint — 0 issues on all new/changed packages
  • CodeQL (security-extended) — 0 findings in-package
  • go mod tidy clean
  • Coverage: provider 75.5%, server 71.3%

Risk & Rollback

Additive: new driver capability + provider/server packages plus opt-in bundle registration (nil driver ⇒ handler not registered). Behavioral changes are limited to more-accurate error codes (dangling refs → InvalidArgument; duplicate final snapshot rejected). Rollback = revert the branch.

Conclusion

MemoryDB reaches full parity — core control plane, multi-region, reserved nodes, service updates, and pagination — with the established driver→provider→server layering. Follow-ups (separate PRs, tracked in the missing-database-server initiative): AWS Timestream & Keyspaces, GCP Spanner & Bigtable, Azure Managed Cassandra.

MemoryDB for Redis/Valkey is a durable, in-VPC cluster service. Unlike
ElastiCache it is control-plane only (no Set/Get data plane), so it gets a
dedicated driver (services/memorydb/driver) rather than reusing the cache
driver, which mandates data-plane methods.

- Driver: 33 core operations (clusters/shards/nodes, ACLs & users, parameter
  groups, subnet groups, snapshots, tags, engine-version & event catalogs)
  plus two type-asserted optional capabilities: MultiRegion (7) and
  ReservedNodes (3).
- Provider (providers/aws/memorydb): in-memory Mock with shard/node/endpoint
  topology, reference validation, restore-from-snapshot, failover, CloudWatch
  metric emission, and clone-on-read on every return path.
- Server (server/aws/memorydb): AWS JSON 1.1 handler on the "AmazonMemoryDB."
  target prefix, with typed faults (*NotFoundFault / *AlreadyExistsFault /
  InvalidParameterValueException). Verified with real aws-sdk-go-v2/memorydb
  round-trip tests plus wire-error assertions.
- Wiring: registered in the AWS provider and server bundles; dedicated
  memorydb:* cost rate keys; docs/services.md section and counts updated.
Comment thread providers/aws/memorydb/clusters.go Fixed
Reject NumShards/ReplicasPerShard outside the MemoryDB service limits
(500 shards, 5 replicas) with InvalidArgument before the mock allocates the
shard/node topology, so an oversized create/update request cannot drive
unbounded memory use. Fixes the CodeQL go/uncontrolled-allocation-size alert.
The caller-side validateShardTopology guard isn't recognized across the
function boundary by CodeQL's uncontrolled-allocation-size analysis. Add a
defensive upper-bound clamp in buildShards itself, right before the slice
allocations, so the bound dominates the make() in-function. Behavior is
unchanged for valid input (callers still reject out-of-range counts first).
CodeQL's uncontrolled-allocation-size analysis does not treat the clamp
reassignment as a sanitizer, so it kept flagging make([]T, 0, n) where n
derives from caller input. Allocate the shard/node slices as nil and let
append grow them; the clamped loop bounds keep growth bounded. No behavior
change for valid input.

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deep end-to-end lifecycle review — AWS MemoryDB (full parity)

Reviewed the whole PR in an isolated worktree (gates, static/coverage, provider lifecycle, server/SDK/typed-faults, cross-cutting wiring), with adversarial verification. The whole MemoryDB lifecycle is supported and working end-to-end — in notably good shape — but not fully gap-free.

Gates: greenbuild ✓ vet ✓ test ✓ -race ✓ gofmt ✓ go mod tidy ✓; lint clean. All 43 driver ops route, 42 are SDK-round-trip-tested against the real aws-sdk-go-v2/service/memorydb client. No High issues. Verified solid: clone-on-read (all paths), single-lock RMW, cascade delete, determinism, create-path reference validation, the buildShards allocation bound (validates ≤500 shards/≤5 replicas before allocation — a genuine fix, not CodeQL-silencing), disjoint X-Amz-Target dispatch (no collision), metrics + cost (tested), and — notably — the documented timestamp hazard is properly mitigated (all timestamps are *time.Time → nil → null, so nothing is ever emitted as an SDK-rejecting RFC3339 string).

Lifecycle scorecard

Stage Status
Cluster create/topology/describe/update/delete + ref validation complete
ACLs & Users (bidirectional linkage + guards) complete
Parameter groups / Subnet groups (in-use + default guards) complete
Snapshots (create/copy/delete/restore) works — restore loses replica count
Shard failover (state guards) works — checks wrong shard index (harmless)
Reserved nodes works — duplicate-purchase fault type wrong
Multi-region clusters CRUD works — parent linkage absent

Medium

  • Multi-region parent linkage is absentDeleteMultiRegionCluster guards len(c.Members) > 0, but Members is never populated (CreateCluster stores MultiRegionClusterName without registering the cluster as a member or validating the MRC exists). The guard is dead code, so an MRC can be deleted while regional clusters still reference it → dangling MultiRegionClusterName (the #303/#304 dangling-link class, in the multi-region path). (inline)
  • Reserved-node duplicate purchase → unmodeled fault — the purchase path maps AlreadyExists via the "ReservedNodesOffering" noun → ReservedNodesOfferingAlreadyExistsFault, which the SDK does not model (only ReservedNodeAlreadyExistsFault exists), so errors.As(&types.ReservedNodeAlreadyExistsFault{}) fails to match. (inline)

Low

  • Restore loses replica countClusterConfiguration has NumShards but no replica field, so a restored cluster is replica-less unless re-specified. (inline)
  • UpdateCluster returns NotFound for a dangling ACL/param-group while CreateCluster returns InvalidArgument — inconsistent code for the same error class (clusters.go).
  • FailoverShard checks Shards[0].NumberOfNodes regardless of the requested shard (harmless while topology is uniform); DeleteCluster with an existing finalSnapshotName silently overwrites it; CreateACL retains duplicate user names.
  • Paging is single-shot (MaxResults/NextToken ignored); DescribeMultiRegionParameters handled-but-untested; the MR param-group error noun is wrong (latent-dead); BatchUpdateCluster/DescribeServiceUpdates unrouted (out of scope, graceful default).
  • The aliasing test covers only the cluster path (code is correct for all); two docs/services.md signature typos (DescribeEngineVersions/DescribeEvents); non-deterministic ListTags order (AWS leaves it unspecified).
  • Coverage below the 90% pillar: provider 73.8%, server 71.8% (disclosed in the PR body).

Verdict: comment. The core cluster/ACL/user/param-group/subnet-group/snapshot lifecycle is complete, correct, and SDK-tested; the two Mediums are fidelity gaps confined to the AWS-only optional surfaces (multi-region linkage, reserved-node fault type).

return nil, cerrors.Newf(cerrors.NotFound, "multi-region cluster %q not found", name)
}

if len(c.Members) > 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] Multi-region parent linkage is absent — this guard is dead code. len(c.Members) > 0 never triggers because Members is never populated: CreateCluster stores cfg.MultiRegionClusterName on the cluster but never appends the cluster to the MRC's Members, and never validates the MRC exists (validateClusterRefs doesn't check it). → Create MRC "m" → create a regional cluster with MultiRegionClusterName="m"DeleteMultiRegionCluster("m") succeeds, leaving the regional cluster with a dangling MultiRegionClusterName pointing at a deleted MRC. Real MemoryDB rejects deleting an MRC with attached regional clusters. Register the cluster into Members on create (and validate the MRC exists), so the guard is live.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7631821. CreateCluster now validates the referenced MRC exists (rejecting a dangling reference with InvalidParameterValueException) and registers the regional cluster into the MRC's Members; DeleteCluster unregisters it. The delete guard is now live — deleting an MRC with attached regional clusters returns FailedPrecondition. Covered by TestMultiRegionParentLinkage (provider).

node, err := rn.PurchaseReservedNodesOffering(r.Context(),
aws.ToString(in.ReservedNodesOfferingId), aws.ToString(in.ReservationId), int(aws.ToInt32(in.NodeCount)))
if err != nil {
writeErr(w, "ReservedNodesOffering", err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] Reserved-node duplicate purchase emits an unmodeled fault. On PurchaseReservedNodesOffering, the provider returns AlreadyExists for a duplicate ReservationId, and this maps via the "ReservedNodesOffering" noun → __type: ReservedNodesOfferingAlreadyExistsFault — which the SDK does not model (its reserved-node faults are ReservedNodeAlreadyExistsFault, ReservedNodeNotFoundFault, ReservedNodeQuotaExceededFault, ReservedNodesOfferingNotFoundFault). → A second purchase with the same ReservationId returns a generic smithy.GenericAPIError, so errors.As(err, &types.ReservedNodeAlreadyExistsFault{}) fails. Map the purchase AlreadyExists to the "ReservedNode" noun (real AWS returns ReservedNodeAlreadyExistsFault); the "ReservedNodesOffering" noun is only correct for the NotFound case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7631821. The purchase path now maps AlreadyExists (duplicate ReservationId) to the ReservedNode noun → ReservedNodeAlreadyExistsFault (SDK-modeled); the NotFound (missing offering) case keeps the ReservedNodesOffering noun. Verified with TestSDKReservedNodeDuplicateFault, which asserts errors.As(&types.ReservedNodeAlreadyExistsFault{}).

}

// ClusterConfiguration captures a cluster's shape at snapshot time.
type ClusterConfiguration struct {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Low] Restore loses the replica count. ClusterConfiguration (the snapshot's stored config) carries NumShards but no replica field, while CreateClusterConfig has NumReplicasPerShard and UpdateCluster has ReplicaCount. → Snapshot a cluster with N replicas → restore via CreateCluster{SnapshotName} → the restored cluster is replica-less (NumberOfNodes=1 per shard, no HA), because the replica count was never captured. Add a replica field to ClusterConfiguration and honor it on restore. (Restore also ignores the snapshot's SubnetGroupName/ParameterGroupName/TLSEnabled.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7631821. Added ReplicasPerShard (and TLSEnabled) to ClusterConfiguration; snapshotFromCluster now captures the replica count and TLS, and CreateCluster's restore path honors ReplicasPerShard/SubnetGroupName/ParameterGroupName/TLSEnabled for any field the caller leaves unset (subnet/param groups only when they still exist). Covered by TestRestorePreservesTopology.

…pagination, service updates

Fixes from NitinKumar004's review plus the two larger parity items:

Medium
- Multi-region parent linkage: CreateCluster now validates the referenced MRC
  exists and registers the regional cluster into its Members; DeleteCluster
  unregisters it. The delete guard is now live — an MRC with attached regional
  clusters can no longer be deleted, and a dangling MultiRegionClusterName can
  no longer be created.
- Reserved-node duplicate purchase now maps AlreadyExists to the ReservedNode
  noun (ReservedNodeAlreadyExistsFault, SDK-modeled) instead of the unmodeled
  ReservedNodesOfferingAlreadyExistsFault; NotFound keeps the Offering noun.

Low
- Restore preserves replica count (new ClusterConfiguration.ReplicasPerShard)
  plus SubnetGroupName/ParameterGroupName/TLSEnabled when the caller omits them.
- UpdateCluster returns InvalidArgument (not NotFound) for a dangling
  ACL/parameter-group, consistent with CreateCluster.
- FailoverShard checks the requested shard's node count, not shard[0].
- DeleteCluster rejects a finalSnapshotName that already exists.
- CreateACL de-duplicates user names.
- Multi-region parameter-group error noun corrected; ListTags returns tags in
  deterministic (sorted) order; docs signature typos fixed.

Full parity additions
- Pagination: every Describe* op honors MaxResults/NextToken via a server-side
  opaque base64 offset token over the deterministic result set; a bad token
  yields InvalidParameterValueException.
- Service updates: new ServiceUpdates optional capability
  (DescribeServiceUpdates + BatchUpdateCluster with partial-success
  UnprocessedClusters), routed through the JSON 1.1 handler.

Edge-case tests added across provider and SDK round-trip layers; docs and counts
updated.
@thzgajendra

Copy link
Copy Markdown
Collaborator Author

Thanks for the deep review, @NitinKumar004 — all Mediums and Lows are addressed in 7631821, and I took the "add both extras" call on the two out-of-scope items.

Medium

  • Multi-region parent linkage — now live (Members populated + validated on create, unregistered on delete). See inline reply.
  • Reserved-node duplicate fault — now ReservedNodeAlreadyExistsFault. See inline reply.

Low

  • Restore replica count (+ subnet/param/TLS) — fixed (inline reply).
  • UpdateCluster dangling ACL/param-group → InvalidArgument, consistent with CreateCluster.
  • FailoverShard now checks the requested shard's node count, not Shards[0].
  • DeleteCluster rejects a finalSnapshotName that already exists (SnapshotAlreadyExistsFault).
  • CreateACL de-duplicates user names.
  • Multi-region parameter-group error noun corrected (MultiRegionParameterGroupNotFoundFault).
  • ListTags returns tags in deterministic (sorted) order.
  • docs/services.md signature typos (DescribeEngineVersions/DescribeEvents) fixed.

Extras added this round (previously flagged out-of-scope)

  • Pagination — every Describe* op now honors MaxResults/NextToken via a server-side opaque base64 offset token over the deterministic result set; a malformed token → InvalidParameterValueException. (TestSDKPagination)
  • Service updates — new ServiceUpdates optional capability: DescribeServiceUpdates + BatchUpdateCluster with partial-success UnprocessedClusters. (TestServiceUpdates, TestSDKServiceUpdates)

Edge-case tests added across the provider and SDK round-trip layers. Coverage: provider 75.5%, server 71.3%. Full local gate green (build/vet/go test ./.../tidy/lint 0 issues/CodeQL 0 findings in-package).

Not changed: NodeUpdateStartDate-style timestamps on ServiceUpdate/Event/reserved nodes remain omitted (nil), for the same AWS-JSON-1.1 epoch-encoding reason documented in the PR — omitting a nullable timestamp keeps SDK deserialization clean.

@NitinKumar004 NitinKumar004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review comments addressed — approving ✅

Verified 7631821 against the earlier review; every finding is resolved, correctly and with tests. Gates green (build ✓ vet ✓ test ✓ -race ✓ gofmt ✓ go mod tidy ✓; lint clean on the MemoryDB files).

Medium — fixed:

  • Multi-region parent linkageCreateCluster now validates the MRC exists and registerMRCMember/unregisterMRCMember populate Members, so the delete-guard is live and dangling MultiRegionClusterName references can no longer occur.
  • Reserved-node duplicate-purchase fault → a duplicate reservation now maps to ReservedNodeAlreadyExistsFault (the SDK-modeled type), so errors.As matches; the *Offering* noun is kept only for the NotFound case.

Low — fixed:

  • Restore now preserves the replica count (ClusterConfiguration.ReplicasPerShard + honored on restore, with AvailabilityMode).
  • FailoverShard checks the requested shard (c.Shards[shard]), not Shards[0].
  • Pagination implemented (paging.go, offset over SortedValues — deterministic) with NextToken on the Describe ops.
  • Multi-region parameter-group error noun corrected to MultiRegionParameterGroup.
  • DescribeServiceUpdates + BatchUpdateCluster implemented (new ServiceUpdates capability) — beyond what the review flagged.

Full MemoryDB lifecycle (cluster/topology → ACLs/users → parameter/subnet groups → snapshots/restore → failover → multi-region → reserved nodes → service updates) is implemented, wired (metrics/cost, disjoint X-Amz-Target dispatch), and round-trips through the real aws-sdk-go-v2/service/memorydb client. Test suite is stable (verified across repeated full runs and a -count=10 stress of the MemoryDB packages). Non-blocking follow-up: provider/server coverage ~72–75% (below the 90% pillar, as disclosed). LGTM.

@thzgajendra
thzgajendra merged commit 57ee301 into stackshy:development Jul 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants