Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from "@effect/vitest"
|
||||
import { Context, Effect, Exit, Fiber, Latch, Layer, Option, Schema } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import {
|
||||
EntityAddress,
|
||||
EntityId,
|
||||
EntityType,
|
||||
Envelope,
|
||||
Message,
|
||||
MessageStorage,
|
||||
Reply,
|
||||
ShardId,
|
||||
ShardingConfig,
|
||||
Snowflake
|
||||
} from "effect/unstable/cluster"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Rpc, RpcSchema } from "effect/unstable/rpc"
|
||||
|
||||
const MemoryLive = MessageStorage.layerMemory.pipe(
|
||||
Layer.provideMerge(Snowflake.layerGenerator),
|
||||
Layer.provide(ShardingConfig.layerDefaults)
|
||||
)
|
||||
|
||||
describe("MessageStorage", () => {
|
||||
describe("memory", () => {
|
||||
it.effect("saves a request", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
const result = yield* storage.saveRequest(request)
|
||||
expect(result._tag).toEqual("Success")
|
||||
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(1)
|
||||
}).pipe(Effect.provide(MemoryLive)))
|
||||
|
||||
it.effect("detects duplicates", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: PrimaryKeyTest,
|
||||
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
const result = yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: PrimaryKeyTest,
|
||||
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
expect(result._tag).toEqual("Duplicate")
|
||||
}).pipe(Effect.provide(MemoryLive)))
|
||||
|
||||
it.effect("unprocessedMessages excludes complete requests", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(0)
|
||||
}).pipe(Effect.provide(MemoryLive)))
|
||||
|
||||
it.effect("repliesFor", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
let replies = yield* storage.repliesFor([request])
|
||||
expect(replies).toHaveLength(0)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
replies = yield* storage.repliesFor([request])
|
||||
expect(replies).toHaveLength(1)
|
||||
expect(replies[0].requestId).toEqual(request.envelope.requestId)
|
||||
}).pipe(Effect.provide(MemoryLive)))
|
||||
|
||||
it.effect("registerReplyHandler", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const latch = yield* Latch.make()
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
const fiber = yield* storage.registerReplyHandler(
|
||||
new Message.OutgoingRequest({
|
||||
...request,
|
||||
respond: () => latch.open
|
||||
})
|
||||
).pipe(Effect.forkChild)
|
||||
yield* TestClock.adjust(1)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
yield* latch.await
|
||||
yield* Fiber.await(fiber)
|
||||
}).pipe(Effect.provide(MemoryLive)))
|
||||
})
|
||||
})
|
||||
|
||||
export const GetUserRpc = Rpc.make("GetUser", {
|
||||
payload: { id: Schema.Number }
|
||||
})
|
||||
|
||||
export const makeRequest = Effect.fnUntraced(function*(options?: {
|
||||
readonly rpc?: Rpc.AnyWithProps
|
||||
readonly payload?: any
|
||||
}) {
|
||||
const snowflake = yield* Snowflake.Generator
|
||||
const rpc = options?.rpc ?? GetUserRpc
|
||||
return new Message.OutgoingRequest({
|
||||
envelope: Envelope.makeRequest<any>({
|
||||
requestId: snowflake.nextUnsafe(),
|
||||
address: EntityAddress.make({
|
||||
shardId: ShardId.make("default", 1),
|
||||
entityType: EntityType.make("test"),
|
||||
entityId: EntityId.make("1")
|
||||
}),
|
||||
tag: rpc._tag,
|
||||
payload: options?.payload ?? { id: 123 },
|
||||
traceId: "noop",
|
||||
spanId: "noop",
|
||||
sampled: false,
|
||||
headers: Headers.empty
|
||||
}),
|
||||
annotations: rpc.annotations,
|
||||
context: Context.empty() as any,
|
||||
rpc,
|
||||
lastReceivedReply: Option.none(),
|
||||
respond() {
|
||||
return Effect.void
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export class PrimaryKeyTest extends Rpc.make("PrimaryKeyTest", {
|
||||
payload: {
|
||||
id: Schema.Number
|
||||
},
|
||||
primaryKey: (value) => value.id.toString()
|
||||
}) {}
|
||||
|
||||
export class StreamRpc extends Rpc.make("StreamTest", {
|
||||
success: RpcSchema.Stream(Schema.Void, Schema.Never),
|
||||
payload: {
|
||||
id: Schema.Number
|
||||
},
|
||||
primaryKey: (value) => value.id.toString()
|
||||
}) {}
|
||||
|
||||
export const makeReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest<any>) {
|
||||
const snowflake = yield* Snowflake.Generator
|
||||
return new Reply.ReplyWithContext({
|
||||
reply: new Reply.WithExit({
|
||||
id: snowflake.nextUnsafe(),
|
||||
requestId: request.envelope.requestId,
|
||||
exit: Exit.void as any
|
||||
}),
|
||||
context: request.context,
|
||||
rpc: request.rpc
|
||||
})
|
||||
})
|
||||
|
||||
export const makeAckChunk = Effect.fnUntraced(function*(
|
||||
request: Message.OutgoingRequest<any>,
|
||||
chunk: Reply.ReplyWithContext<any>
|
||||
) {
|
||||
const snowflake = yield* Snowflake.Generator
|
||||
return new Message.OutgoingEnvelope({
|
||||
envelope: new Envelope.AckChunk({
|
||||
id: snowflake.nextUnsafe(),
|
||||
address: request.envelope.address,
|
||||
requestId: chunk.reply.requestId,
|
||||
replyId: chunk.reply.id
|
||||
}),
|
||||
rpc: request.rpc
|
||||
})
|
||||
})
|
||||
|
||||
export const makeChunkReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest<any>, sequence = 0) {
|
||||
const snowflake = yield* Snowflake.Generator
|
||||
return new Reply.ReplyWithContext({
|
||||
reply: new Reply.Chunk({
|
||||
id: snowflake.nextUnsafe(),
|
||||
requestId: request.envelope.requestId,
|
||||
sequence,
|
||||
values: [undefined]
|
||||
}),
|
||||
context: request.context,
|
||||
rpc: request.rpc
|
||||
})
|
||||
})
|
||||
|
||||
export const makeEmptyReply = (request: Message.OutgoingRequest<any>) => {
|
||||
return new Reply.ReplyWithContext({
|
||||
reply: Reply.Chunk.emptyFrom(request.envelope.requestId),
|
||||
context: request.context,
|
||||
rpc: request.rpc
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { NodeClusterSocket } from "@effect/platform-node"
|
||||
import { describe, it } from "@effect/vitest"
|
||||
import { BigDecimal, Effect, Layer, Option, PrimaryKey, Schema } from "effect"
|
||||
import {
|
||||
ClusterSchema,
|
||||
Entity,
|
||||
MessageStorage,
|
||||
RunnerAddress,
|
||||
RunnerHealth,
|
||||
RunnerStorage,
|
||||
ShardingConfig,
|
||||
SocketRunner
|
||||
} from "effect/unstable/cluster"
|
||||
import { Rpc, RpcSerialization } from "effect/unstable/rpc"
|
||||
|
||||
class TestPayload extends Schema.Class<TestPayload>("TestPayload")({
|
||||
id: Schema.String,
|
||||
amount: Schema.BigDecimal
|
||||
}) {
|
||||
[PrimaryKey.symbol]() {
|
||||
return this.id
|
||||
}
|
||||
}
|
||||
|
||||
const TestEntity = Entity
|
||||
.make("TestEntity", [
|
||||
Rpc.make("Process", {
|
||||
payload: TestPayload,
|
||||
success: Schema.Void
|
||||
})
|
||||
])
|
||||
.annotateRpcs(ClusterSchema.Persisted, true)
|
||||
.annotateRpcs(ClusterSchema.Uninterruptible, true)
|
||||
|
||||
const TestEntityLayer = TestEntity.toLayer(
|
||||
Effect.succeed({
|
||||
Process: () => Effect.void
|
||||
})
|
||||
)
|
||||
|
||||
const RUNNER_PORT = 50_123
|
||||
// Build shared storage instances once, so runner and client see the same state.
|
||||
// MessageStorage.layerMemory requires ShardingConfig, so we provide a minimal one.
|
||||
const SharedStorage = Layer.mergeAll(
|
||||
RunnerStorage.layerMemory,
|
||||
MessageStorage.layerMemory
|
||||
).pipe(
|
||||
Layer.provide(ShardingConfig.layerDefaults)
|
||||
)
|
||||
|
||||
const makeRunnerLayer = (port: number) =>
|
||||
TestEntityLayer.pipe(
|
||||
Layer.provideMerge(SocketRunner.layer),
|
||||
Layer.provide(RunnerHealth.layerNoop),
|
||||
Layer.provide(NodeClusterSocket.layerSocketServer),
|
||||
Layer.provide(NodeClusterSocket.layerClientProtocol),
|
||||
Layer.provide(ShardingConfig.layer({
|
||||
runnerAddress: Option.some(RunnerAddress.make("localhost", port)),
|
||||
entityTerminationTimeout: 0,
|
||||
entityMessagePollInterval: 5000,
|
||||
sendRetryInterval: 100
|
||||
})),
|
||||
Layer.provide(RpcSerialization.layerMsgPack)
|
||||
)
|
||||
|
||||
const makeClientLayer = (port: number) =>
|
||||
SocketRunner.layerClientOnly.pipe(
|
||||
Layer.provide(NodeClusterSocket.layerClientProtocol),
|
||||
Layer.provide(ShardingConfig.layer({
|
||||
runnerAddress: Option.some(RunnerAddress.make("localhost", port)),
|
||||
runnerListenAddress: Option.some(RunnerAddress.make("localhost", port)),
|
||||
entityTerminationTimeout: 0,
|
||||
entityMessagePollInterval: 5000,
|
||||
sendRetryInterval: 100
|
||||
})),
|
||||
Layer.provide(RpcSerialization.layerMsgPack)
|
||||
)
|
||||
|
||||
// BigDecimal.normalize creates a circular `normalized` self-reference.
|
||||
// When a persisted message is sent with discard: true, the notify path in Runners.makeRpc
|
||||
// passes the raw envelope (with circular BigDecimal payload) to the runner via msgpack,
|
||||
// causing RangeError: Maximum call stack size exceeded.
|
||||
describe("SocketRunner", () => {
|
||||
it.live(
|
||||
"entity call with BigDecimal and discard should not stack overflow",
|
||||
() =>
|
||||
Effect.gen(function*() {
|
||||
// Start the runner (with socket server and entity handler)
|
||||
yield* Layer.launch(makeRunnerLayer(RUNNER_PORT)).pipe(Effect.forkScoped)
|
||||
|
||||
// Give the runner time to start and acquire shards
|
||||
yield* Effect.sleep("2 seconds")
|
||||
yield* Effect.log("Before starting the client")
|
||||
|
||||
// Send a message from the client with discard: true.
|
||||
// The BigDecimal is normalized to trigger the circular `normalized` self-reference.
|
||||
yield* Effect.gen(function*() {
|
||||
yield* Effect.log("Starting the client")
|
||||
yield* Effect.sleep("2 seconds")
|
||||
const makeClient = yield* TestEntity.client
|
||||
// Give the client time to discover the runner
|
||||
yield* Effect.sleep("3 seconds")
|
||||
const client = makeClient("entity-1")
|
||||
|
||||
const amount = BigDecimal.fromStringUnsafe("123.45")
|
||||
|
||||
yield* client.Process(
|
||||
TestPayload.make({ id: "req-1", amount }),
|
||||
{ discard: true }
|
||||
)
|
||||
}).pipe(
|
||||
Effect.provide(makeClientLayer(RUNNER_PORT)),
|
||||
Effect.scoped
|
||||
)
|
||||
}).pipe(Effect.provide(
|
||||
SharedStorage
|
||||
)),
|
||||
30_000
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,222 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { assert, describe, expect, it } from "@effect/vitest"
|
||||
import { Effect, Fiber, FileSystem, Latch, Layer, Option } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { Message, MessageStorage, ShardingConfig, Snowflake, SqlMessageStorage } from "effect/unstable/cluster"
|
||||
import { SqlClient } from "effect/unstable/sql"
|
||||
import { MysqlContainer } from "../fixtures/mysql2-utils.ts"
|
||||
import { PgContainer } from "../fixtures/pg-utils.ts"
|
||||
import {
|
||||
makeAckChunk,
|
||||
makeChunkReply,
|
||||
makeReply,
|
||||
makeRequest,
|
||||
PrimaryKeyTest,
|
||||
StreamRpc
|
||||
} from "./MessageStorageTest.ts"
|
||||
|
||||
const StorageLive = SqlMessageStorage.layer.pipe(
|
||||
Layer.provideMerge(Snowflake.layerGenerator),
|
||||
Layer.provide(ShardingConfig.layerDefaults)
|
||||
)
|
||||
|
||||
const truncate = Effect.gen(function*() {
|
||||
const sql = yield* SqlClient.SqlClient
|
||||
yield* sql`DELETE FROM cluster_replies`
|
||||
yield* sql`DELETE FROM cluster_messages`
|
||||
})
|
||||
|
||||
describe("SqlMessageStorage", () => {
|
||||
;([
|
||||
["pg", Layer.orDie(PgContainer.layerClient)],
|
||||
["mysql", Layer.orDie(MysqlContainer.layerClient)],
|
||||
["sqlite", Layer.orDie(SqliteLayer)]
|
||||
] as const).forEach(([label, layer]) => {
|
||||
it.layer(StorageLive.pipe(Layer.provideMerge(layer)), {
|
||||
timeout: 120000
|
||||
})(label, (it) => {
|
||||
it.effect("saveRequest", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest({ payload: { id: 1 } })
|
||||
const result = yield* storage.saveRequest(request)
|
||||
expect(result._tag).toEqual("Success")
|
||||
|
||||
for (let i = 2; i <= 5; i++) {
|
||||
yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } }))
|
||||
}
|
||||
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
|
||||
let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(4)
|
||||
expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([2, 3, 4, 5])
|
||||
|
||||
for (let i = 6; i <= 10; i++) {
|
||||
yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } }))
|
||||
}
|
||||
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(5)
|
||||
expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([6, 7, 8, 9, 10])
|
||||
}))
|
||||
|
||||
it.effect("saveReply + saveRequest duplicate", () =>
|
||||
Effect.gen(function*() {
|
||||
const sql = yield* SqlClient.SqlClient
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest({
|
||||
rpc: StreamRpc,
|
||||
payload: StreamRpc.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
let result = yield* storage.saveRequest(request)
|
||||
expect(result._tag).toEqual("Success")
|
||||
|
||||
let chunk = yield* makeChunkReply(request, 0)
|
||||
yield* storage.saveReply(chunk)
|
||||
const ackChunk = yield* makeAckChunk(request, chunk)
|
||||
yield* storage.saveEnvelope(ackChunk)
|
||||
|
||||
chunk = yield* makeChunkReply(request, 1)
|
||||
yield* storage.saveReply(chunk)
|
||||
|
||||
result = yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: StreamRpc,
|
||||
payload: StreamRpc.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply))
|
||||
expect(result.lastReceivedReply.value._tag).toEqual("Chunk")
|
||||
|
||||
// get the un-acked chunk
|
||||
const replies = yield* storage.repliesFor([request])
|
||||
expect(replies).toHaveLength(1)
|
||||
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
|
||||
result = yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: StreamRpc,
|
||||
payload: StreamRpc.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply))
|
||||
expect(result.lastReceivedReply.value._tag).toEqual("WithExit")
|
||||
|
||||
// duplicate WithExit
|
||||
const fiber = yield* storage.saveReply(yield* makeReply(request)).pipe(Effect.forkChild)
|
||||
yield* TestClock.adjust(1)
|
||||
while (!fiber.pollUnsafe()) {
|
||||
yield* sql`SELECT 1`
|
||||
yield* TestClock.adjust(1000)
|
||||
}
|
||||
const error = yield* Effect.flip(Fiber.join(fiber))
|
||||
expect(error._tag).toEqual("PersistenceError")
|
||||
}))
|
||||
|
||||
it.effect("detects duplicates", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* truncate
|
||||
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: PrimaryKeyTest,
|
||||
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
const result = yield* storage.saveRequest(
|
||||
yield* makeRequest({
|
||||
rpc: PrimaryKeyTest,
|
||||
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
|
||||
})
|
||||
)
|
||||
expect(result._tag).toEqual("Duplicate")
|
||||
}))
|
||||
|
||||
it.effect("unprocessedMessages", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* truncate
|
||||
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(1)
|
||||
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(0)
|
||||
yield* storage.saveRequest(yield* makeRequest())
|
||||
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(1)
|
||||
}))
|
||||
|
||||
it.effect("unprocessedMessages excludes complete requests", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* truncate
|
||||
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
|
||||
expect(messages).toHaveLength(0)
|
||||
}))
|
||||
|
||||
it.effect("repliesFor", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* truncate
|
||||
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
let replies = yield* storage.repliesFor([request])
|
||||
expect(replies).toHaveLength(0)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
replies = yield* storage.repliesFor([request])
|
||||
expect(replies).toHaveLength(1)
|
||||
expect(replies[0].requestId).toEqual(request.envelope.requestId)
|
||||
}))
|
||||
|
||||
it.effect("registerReplyHandler", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const latch = yield* Latch.make()
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
const fiber = yield* storage.registerReplyHandler(
|
||||
new Message.OutgoingRequest({
|
||||
...request,
|
||||
respond: () => latch.open
|
||||
})
|
||||
).pipe(Effect.forkChild)
|
||||
yield* TestClock.adjust(1)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
yield* latch.await
|
||||
yield* Fiber.await(fiber)
|
||||
}))
|
||||
|
||||
it.effect("unprocessedMessagesById", () =>
|
||||
Effect.gen(function*() {
|
||||
yield* truncate
|
||||
|
||||
const storage = yield* MessageStorage.MessageStorage
|
||||
const request = yield* makeRequest()
|
||||
yield* storage.saveRequest(request)
|
||||
let messages = yield* storage.unprocessedMessagesById([request.envelope.requestId])
|
||||
expect(messages).toHaveLength(1)
|
||||
yield* storage.saveReply(yield* makeReply(request))
|
||||
messages = yield* storage.unprocessedMessagesById([request.envelope.requestId])
|
||||
expect(messages).toHaveLength(0)
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const SqliteLayer = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return SqliteClient.layer({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer))
|
||||
@@ -0,0 +1,101 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SqliteClient } from "@effect/sql-sqlite-node"
|
||||
import { describe, expect, it } from "@effect/vitest"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import {
|
||||
Runner,
|
||||
RunnerAddress,
|
||||
RunnerStorage,
|
||||
ShardId,
|
||||
ShardingConfig,
|
||||
SqlRunnerStorage
|
||||
} from "effect/unstable/cluster"
|
||||
import { MysqlContainer } from "../fixtures/mysql2-utils.ts"
|
||||
import { PgContainer } from "../fixtures/pg-utils.ts"
|
||||
|
||||
const StorageLive = SqlRunnerStorage.layer
|
||||
|
||||
describe("SqlRunnerStorage", () => {
|
||||
;([
|
||||
["pg", Layer.orDie(PgContainer.layerClient)],
|
||||
["mysql", Layer.orDie(MysqlContainer.layerClient)],
|
||||
["vitess", Layer.orDie(MysqlContainer.layerClientVitess)],
|
||||
["sqlite", Layer.orDie(SqliteLayer)]
|
||||
] as const).flatMap(([label, layer]) =>
|
||||
[
|
||||
[label, StorageLive.pipe(Layer.provideMerge(layer), Layer.provide(ShardingConfig.layer()))],
|
||||
[
|
||||
label + " (no advisory)",
|
||||
StorageLive.pipe(
|
||||
Layer.provideMerge(layer),
|
||||
Layer.provide(ShardingConfig.layer({
|
||||
shardLockDisableAdvisory: true
|
||||
}))
|
||||
)
|
||||
]
|
||||
] as const
|
||||
).forEach(([label, layer]) => {
|
||||
it.layer(layer, {
|
||||
timeout: 60000
|
||||
})(label, (it) => {
|
||||
it.effect("getRunners", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* RunnerStorage.RunnerStorage
|
||||
|
||||
const runner = Runner.make({
|
||||
address: runnerAddress1,
|
||||
groups: ["default"],
|
||||
weight: 1
|
||||
})
|
||||
const machineId = yield* storage.register(runner, true)
|
||||
yield* storage.register(runner, true)
|
||||
expect(machineId).toEqual(1)
|
||||
expect(yield* storage.getRunners).toEqual([[runner, true]])
|
||||
|
||||
yield* storage.setRunnerHealth(runnerAddress1, false)
|
||||
expect(yield* storage.getRunners).toEqual([[runner, false]])
|
||||
|
||||
yield* storage.unregister(runnerAddress1)
|
||||
expect(yield* storage.getRunners).toEqual([])
|
||||
}), 30_000)
|
||||
|
||||
it.effect("acquireShards", () =>
|
||||
Effect.gen(function*() {
|
||||
const storage = yield* RunnerStorage.RunnerStorage
|
||||
|
||||
let acquired = yield* storage.acquire(runnerAddress1, [
|
||||
ShardId.make("default", 1),
|
||||
ShardId.make("default", 2),
|
||||
ShardId.make("default", 3)
|
||||
])
|
||||
expect(acquired.map((_) => _.id)).toEqual([1, 2, 3])
|
||||
acquired = yield* storage.acquire(runnerAddress1, [
|
||||
ShardId.make("default", 1),
|
||||
ShardId.make("default", 2),
|
||||
ShardId.make("default", 3)
|
||||
])
|
||||
expect(acquired.map((_) => _.id)).toEqual([1, 2, 3])
|
||||
|
||||
const refreshed = yield* storage.refresh(runnerAddress1, [
|
||||
ShardId.make("default", 1),
|
||||
ShardId.make("default", 2),
|
||||
ShardId.make("default", 3)
|
||||
])
|
||||
expect(refreshed.map((_) => _.id)).toEqual([1, 2, 3])
|
||||
|
||||
// smoke test release
|
||||
yield* storage.release(runnerAddress1, ShardId.make("default", 2))
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const runnerAddress1 = RunnerAddress.make("localhost", 1234)
|
||||
|
||||
const SqliteLayer = Effect.gen(function*() {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const dir = yield* fs.makeTempDirectoryScoped()
|
||||
return SqliteClient.layer({
|
||||
filename: dir + "/test.db"
|
||||
})
|
||||
}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer))
|
||||
Reference in New Issue
Block a user