1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as ElastiCache from "@distilled.cloud/aws/elasticache";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import {
8 assertReplicationGroupGone,
9 getProvisionedNetwork,
10 shareProvisionedNetwork,
11} from "./ProvisionedFixture.ts";
12const { test, beforeAll, afterAll } = Test.make({
13 providers: AWS.providers(),
14});
15shareProvisionedNetwork({ beforeAll, afterAll });
16const waitForAvailable = (replicationGroupId: string) =>
17 ElastiCache.describeReplicationGroups({
18 ReplicationGroupId: replicationGroupId,
19 }).pipe(
20 Effect.flatMap((response) =>
21 response.ReplicationGroups?.[0]?.Status === "available"
22 ? Effect.succeed(response.ReplicationGroups[0])
23 : Effect.fail(
24 new Error(
25 `replication group '${replicationGroupId}' is not available`,
26 ),
27 ),
28 ),
29 Effect.retry({
30 schedule: Schedule.max([
31 Schedule.fixed("15 seconds"),
32 Schedule.recurs(60),
33 ]),
34 }),
35 );
36
37test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
38 "adds a Valkey replica online and enables high availability",
39 (stack) =>
40 Effect.gen(function* () {
41 yield* stack.destroy();
42 const deploy = (numNodeGroups: number, replicasPerNodeGroup: number) =>
43 stack.deploy(
44 Effect.gen(function* () {
45 const net = yield* getProvisionedNetwork;
46 const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
47 description: "alchemy topology cache",
48 engine: "valkey",
49 nodeType: "cache.t4g.micro",
50 subnetGroupName: net.subnetGroupName,
51 securityGroupIds: [net.securityGroupId],
52 numNodeGroups,
53 replicasPerNodeGroup,
54 automaticFailoverEnabled: replicasPerNodeGroup > 0,
55 multiAzEnabled: replicasPerNodeGroup > 0,
56 transitEncryptionEnabled: true,
57 });
58 return { cache };
59 }),
60 );
61
62 const created = yield* deploy(1, 0);
63 const withReplica = yield* deploy(1, 1);
64 const replicaGroup = yield* waitForAvailable(
65 withReplica.cache.replicationGroupId,
66 );
67 expect(replicaGroup.AutomaticFailover).toBe("enabled");
68 expect(replicaGroup.MultiAZ).toBe("enabled");
69 expect(replicaGroup.NodeGroups?.[0]?.NodeGroupMembers).toHaveLength(2);
70
71 yield* stack.destroy();
72 yield* assertReplicationGroupGone(created.cache.replicationGroupId);
73 }),
74 {
75 tags: [
76 "provider:aws",
77 "provider:aws:ec2",
78 "provider:aws:elasticache",
79 "live",
80 ],
81 timeout: 2_700_000,
82 },
83);
84
85test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
86 "scales shards online for a cluster-mode-enabled Valkey group",
87 (stack) =>
88 Effect.gen(function* () {
89 yield* stack.destroy();
90 const deploy = (numNodeGroups: number) =>
91 stack.deploy(
92 Effect.gen(function* () {
93 const net = yield* getProvisionedNetwork;
94 const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
95 description: "alchemy shard topology cache",
96 engine: "valkey",
97 nodeType: "cache.t4g.micro",
98 subnetGroupName: net.subnetGroupName,
99 securityGroupIds: [net.securityGroupId],
100 numNodeGroups,
101 replicasPerNodeGroup: 0,
102 transitEncryptionEnabled: true,
103 });
104 return { cache };
105 }),
106 );
107
108 const created = yield* deploy(2);
109 const scaled = yield* deploy(3);
110 const group = yield* waitForAvailable(scaled.cache.replicationGroupId);
111 expect(group.NodeGroups).toHaveLength(3);
112 expect(
113 group.NodeGroups?.every(
114 (shard) => shard.NodeGroupMembers?.length === 1,
115 ),
116 ).toBe(true);
117
118 yield* stack.destroy();
119 yield* assertReplicationGroupGone(created.cache.replicationGroupId);
120 }),
121 {
122 tags: [
123 "provider:aws",
124 "provider:aws:ec2",
125 "provider:aws:elasticache",
126 "live",
127 ],
128 timeout: 2_700_000,
129 },
130);
131
132test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
133 "reconciles a highly available replication group",
134 (stack) =>
135 Effect.gen(function* () {
136 yield* stack.destroy();
137 const deploy = () =>
138 stack.deploy(
139 Effect.gen(function* () {
140 const net = yield* getProvisionedNetwork;
141 const cache = yield* AWS.ElastiCache.ReplicationGroup("Cache", {
142 description: "alchemy failover cache",
143 engine: "valkey",
144 nodeType: "cache.t4g.micro",
145 subnetGroupName: net.subnetGroupName,
146 securityGroupIds: [net.securityGroupId],
147 replicasPerNodeGroup: 1,
148 automaticFailoverEnabled: true,
149 multiAzEnabled: true,
150 transitEncryptionEnabled: true,
151 });
152 return { cache };
153 }),
154 );
155
156 const { cache } = yield* deploy();
157
158 const before = yield* waitForAvailable(cache.replicationGroupId);
159 const nodeGroupId = before.NodeGroups?.[0]?.NodeGroupId;
160 if (!nodeGroupId)
161 return yield* Effect.fail(
162 new Error("replication group has no node group"),
163 );
164 yield* ElastiCache.testFailover({
165 ReplicationGroupId: cache.replicationGroupId,
166 NodeGroupId: nodeGroupId,
167 });
168 const reconciled = yield* deploy();
169 expect(reconciled.cache.replicationGroupId).toBe(
170 cache.replicationGroupId,
171 );
172 const after = yield* waitForAvailable(cache.replicationGroupId);
173 expect(after.AutomaticFailover).toBe("enabled");
174 expect(after.MultiAZ).toBe("enabled");
175 expect(after.NodeGroups?.[0]?.NodeGroupMembers).toHaveLength(2);
176
177 yield* stack.destroy();
178 yield* assertReplicationGroupGone(cache.replicationGroupId);
179 }),
180 {
181 tags: [
182 "provider:aws",
183 "provider:aws:ec2",
184 "provider:aws:elasticache",
185 "live",
186 ],
187 timeout: 2_700_000,
188 },
189);