@lota-sdk/core 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -12
- package/src/ai/embedding-cache.ts +94 -22
- package/src/ai-gateway/ai-gateway.ts +738 -223
- package/src/config/agent-defaults.ts +176 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/constants.ts +8 -2
- package/src/config/logger.ts +286 -19
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -383
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +856 -598
- package/src/db/memory.ts +398 -275
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +255 -0
- package/src/db/service.ts +726 -761
- package/src/db/startup.ts +140 -66
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +87 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +98 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/layers.ts +228 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +31 -0
- package/src/effect/services.ts +57 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +122 -71
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +29 -0
- package/src/queues/autonomous-job.queue.ts +130 -74
- package/src/queues/context-compaction.queue.ts +60 -15
- package/src/queues/delayed-node-promotion.queue.ts +52 -15
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +13 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
- package/src/queues/plan-scheduler.queue.ts +107 -31
- package/src/queues/post-chat-memory.queue.ts +66 -24
- package/src/queues/queue-factory.ts +142 -52
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +54 -9
- package/src/redis/connection.ts +84 -32
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +10 -0
- package/src/redis/stream-context.ts +84 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +4 -1
- package/src/runtime/agent-stream-helpers.ts +20 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +11 -7
- package/src/runtime/helper-model.ts +135 -48
- package/src/runtime/index.ts +7 -7
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
- package/src/runtime/plugin-resolution.ts +144 -24
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +197 -130
- package/src/runtime/retrieval-adapters.ts +38 -4
- package/src/runtime/runtime-config.ts +165 -61
- package/src/runtime/runtime-extensions.ts +21 -34
- package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
- package/src/runtime/social-chat/social-chat.ts +594 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +172 -94
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +329 -217
- package/src/services/artifact.service.ts +225 -148
- package/src/services/attachment.service.ts +137 -115
- package/src/services/autonomous-job.service.ts +888 -491
- package/src/services/chat-run-registry.service.ts +11 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +162 -90
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +256 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +80 -170
- package/src/services/graph-full-routing.ts +182 -0
- package/src/services/index.ts +18 -20
- package/src/services/institutional-memory.service.ts +220 -123
- package/src/services/learned-skill.service.ts +364 -259
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-org-memory.ts +39 -0
- package/src/services/memory/memory-preseeded.ts +80 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
- package/src/services/memory/memory.service.ts +692 -0
- package/src/services/memory/rerank.service.ts +209 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +17 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +144 -51
- package/src/services/ownership-dispatcher.service.ts +415 -264
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/plan/plan-approval.service.ts +102 -0
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +175 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +398 -0
- package/src/services/plan/plan-deadline.service.ts +547 -0
- package/src/services/plan/plan-event-delivery.service.ts +261 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +475 -0
- package/src/services/plan/plan-executor-helpers.ts +322 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1654 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +644 -0
- package/src/services/plan/plan-scheduler.service.ts +385 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +33 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +125 -0
- package/src/services/plugin-executor.service.ts +97 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +296 -230
- package/src/services/recent-activity-title.service.ts +65 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +176 -125
- package/src/services/system-executor.service.ts +91 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +369 -0
- package/src/services/thread/thread-listing.ts +198 -0
- package/src/services/thread/thread-memory-block.ts +117 -0
- package/src/services/thread/thread-message.service.ts +363 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
- package/src/services/thread/thread-turn-streaming.ts +402 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +343 -0
- package/src/services/thread/thread.service.ts +335 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +331 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +2 -2
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/memory-reranker.agent.ts +2 -2
- package/src/system-agents/memory.agent.ts +2 -2
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
- package/src/system-agents/skill-extractor.agent.ts +2 -2
- package/src/system-agents/skill-manager.agent.ts +2 -2
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +220 -161
- package/src/tools/fetch-webpage.tool.ts +21 -17
- package/src/tools/firecrawl-client.ts +16 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +49 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +26 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +124 -83
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +17 -23
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +95 -16
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +186 -51
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +56 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -844
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-approval.service.ts +0 -83
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
package/src/redis/connection.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Duration, Effect, Exit, Fiber } from 'effect'
|
|
1
2
|
import IORedis from 'ioredis'
|
|
2
3
|
import type { RedisOptions } from 'ioredis'
|
|
3
4
|
|
|
5
|
+
import { ConfigurationError } from '../effect/errors'
|
|
6
|
+
import { effectTryServicePromise } from '../effect/helpers'
|
|
4
7
|
import { getErrorMessage } from '../utils/errors'
|
|
5
8
|
|
|
6
9
|
export interface RedisConnectionLogger {
|
|
@@ -10,7 +13,7 @@ export interface RedisConnectionLogger {
|
|
|
10
13
|
error?: (message: string) => void
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
interface CreateRedisConnectionManagerOptions {
|
|
16
|
+
export interface CreateRedisConnectionManagerOptions {
|
|
14
17
|
url: string
|
|
15
18
|
redisOptions?: RedisOptions
|
|
16
19
|
healthCheckIntervalMs?: number
|
|
@@ -29,6 +32,8 @@ const REDIS_RETRY_STEP_MS = 50
|
|
|
29
32
|
const REDIS_RETRY_MAX_DELAY_MS = 2000
|
|
30
33
|
const REDIS_CONNECT_TIMEOUT_MS = 10_000
|
|
31
34
|
|
|
35
|
+
type HealthCheckFiber = ReturnType<typeof Effect.runFork>
|
|
36
|
+
|
|
32
37
|
export const DEFAULT_REDIS_OPTIONS: RedisOptions = {
|
|
33
38
|
maxRetriesPerRequest: null,
|
|
34
39
|
enableReadyCheck: true,
|
|
@@ -55,10 +60,11 @@ function log(logger: RedisConnectionLogger | undefined, level: keyof RedisConnec
|
|
|
55
60
|
class RedisConnectionManagerImpl implements RedisConnectionManager {
|
|
56
61
|
private redis: IORedis | null = null
|
|
57
62
|
private isHealthy = false
|
|
58
|
-
private healthCheckInterval: ReturnType<typeof setInterval> | null = null
|
|
59
63
|
private isInitialized = false
|
|
60
64
|
private isClosing = false
|
|
61
65
|
private isHealthCheckRunning = false
|
|
66
|
+
private healthChecksActive = false
|
|
67
|
+
private healthCheckFiber: HealthCheckFiber | null = null
|
|
62
68
|
|
|
63
69
|
constructor(private readonly options: CreateRedisConnectionManagerOptions) {
|
|
64
70
|
this.initializeConnection()
|
|
@@ -123,32 +129,70 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
|
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
private startHealthChecks(): void {
|
|
126
|
-
if (this.
|
|
132
|
+
if (this.healthChecksActive || this.healthCheckFiber) {
|
|
127
133
|
return
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
this.healthChecksActive = true
|
|
130
137
|
const intervalMs = this.options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
const logger = this.options.logger
|
|
139
|
+
const isHealthChecksActive = () => this.healthChecksActive
|
|
140
|
+
const isHealthCheckRunning = () => this.isHealthCheckRunning
|
|
141
|
+
const setHealthCheckRunning = (value: boolean) => {
|
|
142
|
+
this.isHealthCheckRunning = value
|
|
143
|
+
}
|
|
144
|
+
const getRedis = () => this.redis
|
|
145
|
+
const setHealthy = (value: boolean) => {
|
|
146
|
+
this.isHealthy = value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.healthCheckFiber = Effect.runFork(
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
for (;;) {
|
|
152
|
+
yield* Effect.sleep(Duration.millis(intervalMs))
|
|
153
|
+
if (!isHealthChecksActive()) break
|
|
154
|
+
if (isHealthCheckRunning()) continue
|
|
155
|
+
|
|
156
|
+
setHealthCheckRunning(true)
|
|
157
|
+
try {
|
|
158
|
+
const redis = getRedis()
|
|
159
|
+
if (redis && redis.status === 'ready') {
|
|
160
|
+
const pingExit = yield* Effect.exit(
|
|
161
|
+
effectTryServicePromise(() => redis.ping(), 'Redis health check failed'),
|
|
162
|
+
)
|
|
163
|
+
if (Exit.isFailure(pingExit)) {
|
|
164
|
+
log(logger, 'warn', `Redis health check failed: ${getErrorMessage(pingExit.cause)}`)
|
|
165
|
+
setHealthy(false)
|
|
166
|
+
} else {
|
|
167
|
+
setHealthy(true)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
setHealthCheckRunning(false)
|
|
172
|
+
}
|
|
138
173
|
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private stopHealthChecks(): Promise<void> {
|
|
179
|
+
this.healthChecksActive = false
|
|
180
|
+
|
|
181
|
+
const fiber = this.healthCheckFiber
|
|
182
|
+
if (!fiber) {
|
|
183
|
+
return Promise.resolve()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.healthCheckFiber = null
|
|
187
|
+
return Effect.runPromise(Fiber.interrupt(fiber)).then(
|
|
188
|
+
() => undefined,
|
|
189
|
+
() => undefined,
|
|
190
|
+
)
|
|
147
191
|
}
|
|
148
192
|
|
|
149
193
|
getConnection(): IORedis {
|
|
150
194
|
if (!this.redis) {
|
|
151
|
-
throw new
|
|
195
|
+
throw new ConfigurationError({ message: 'Redis connection not initialized', key: 'redis' })
|
|
152
196
|
}
|
|
153
197
|
return this.redis
|
|
154
198
|
}
|
|
@@ -161,22 +205,30 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
|
|
|
161
205
|
return this.isHealthy && this.redis?.status === 'ready'
|
|
162
206
|
}
|
|
163
207
|
|
|
164
|
-
|
|
165
|
-
if (this.healthCheckInterval) {
|
|
166
|
-
clearInterval(this.healthCheckInterval)
|
|
167
|
-
this.healthCheckInterval = null
|
|
168
|
-
}
|
|
169
|
-
|
|
208
|
+
closeConnection(): Promise<void> {
|
|
170
209
|
this.isClosing = true
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
log(this.options.logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(error)}`)
|
|
210
|
+
const logger = this.options.logger
|
|
211
|
+
const getRedis = () => this.redis
|
|
212
|
+
const stopHealthChecks = () => this.stopHealthChecks()
|
|
213
|
+
const disconnect = () => {
|
|
178
214
|
this.redis?.disconnect()
|
|
179
215
|
}
|
|
216
|
+
|
|
217
|
+
return Effect.runPromise(
|
|
218
|
+
Effect.gen(function* () {
|
|
219
|
+
yield* Effect.tryPromise(() => stopHealthChecks())
|
|
220
|
+
const redis = getRedis()
|
|
221
|
+
if (redis && redis.status !== 'end') {
|
|
222
|
+
const quitExit = yield* Effect.exit(
|
|
223
|
+
effectTryServicePromise(() => redis.quit(), 'Failed to close Redis connection manager'),
|
|
224
|
+
)
|
|
225
|
+
if (Exit.isFailure(quitExit)) {
|
|
226
|
+
log(logger, 'warn', `Redis quit failed, forcing disconnect: ${getErrorMessage(quitExit.cause)}`)
|
|
227
|
+
disconnect()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
)
|
|
180
232
|
}
|
|
181
233
|
}
|
|
182
234
|
|
package/src/redis/index.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { createRedisConnectionManager } from './connection'
|
|
2
2
|
import type { RedisConnectionManager } from './connection'
|
|
3
3
|
export { DEFAULT_REDIS_OPTIONS, type RedisConnectionLogger } from './connection'
|
|
4
|
+
export { withOrgMemoryLock, withOrgMemoryLockEffect } from './org-memory-lock'
|
|
5
|
+
export { withLeaseLock } from './redis-lease-lock'
|
|
4
6
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './connection-accessor'
|
|
10
|
-
export { withOrgMemoryLock } from './org-memory-lock'
|
|
11
|
-
export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
|
|
12
|
-
export { closeSharedSubscriber, createThreadResumableContext } from './stream-context'
|
|
7
|
+
createThreadResumableContext,
|
|
8
|
+
SharedThreadStreamSubscriberLive,
|
|
9
|
+
SharedThreadStreamSubscriberTag,
|
|
10
|
+
} from './stream-context'
|
|
13
11
|
|
|
14
12
|
export { createRedisConnectionManager }
|
|
15
13
|
export type { RedisConnectionManager }
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { Schema, Effect } from 'effect'
|
|
2
|
+
import type IORedis from 'ioredis'
|
|
3
|
+
|
|
1
4
|
import { serverLogger } from '../config/logger'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
5
|
+
import { LockAcquisitionError } from '../effect/errors'
|
|
6
|
+
import type { LockLostError, RedisError } from '../effect/errors'
|
|
7
|
+
import { withLeaseLock } from './redis-lease-lock'
|
|
8
|
+
import type { RedisLeaseLockOptions } from './redis-lease-lock'
|
|
9
|
+
import { getRuntimeRedisConnection } from './runtime-connection'
|
|
4
10
|
|
|
5
11
|
const ORG_MEMORY_LOCK_PREFIX = 'lock:org-memory:org:'
|
|
6
12
|
const ORG_MEMORY_LOCK_TTL_MS = 120_000
|
|
@@ -9,35 +15,62 @@ const ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS = 30_000
|
|
|
9
15
|
const ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS = 30_000
|
|
10
16
|
const ORG_MEMORY_LOCK_MAX_WAIT_MS = 15 * 60 * 1000
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
class OrgMemoryLockCallbackError extends Schema.TaggedErrorClass<OrgMemoryLockCallbackError>()(
|
|
19
|
+
'OrgMemoryLockCallbackError',
|
|
20
|
+
{ message: Schema.String, cause: Schema.Defect },
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
function createOrgMemoryLockOptions(redis: IORedis, orgId: string): RedisLeaseLockOptions & { redis: IORedis } {
|
|
24
|
+
return {
|
|
25
|
+
redis,
|
|
26
|
+
lockKey: `${ORG_MEMORY_LOCK_PREFIX}${orgId}`,
|
|
27
|
+
lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
|
|
28
|
+
retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
|
|
29
|
+
refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
|
|
30
|
+
waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
|
|
31
|
+
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
32
|
+
label: 'org memory lock',
|
|
33
|
+
logger: {
|
|
34
|
+
debug: (message) => {
|
|
35
|
+
serverLogger.debug`${message}`
|
|
36
|
+
},
|
|
37
|
+
info: (message) => {
|
|
38
|
+
serverLogger.info`${message}`
|
|
39
|
+
},
|
|
40
|
+
warn: (message) => {
|
|
41
|
+
serverLogger.warn`${message}`
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function withOrgMemoryLock<T>(orgId: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
48
|
+
return Effect.runPromise(
|
|
49
|
+
withOrgMemoryLockEffect(orgId, (signal) =>
|
|
50
|
+
Effect.tryPromise({
|
|
51
|
+
try: () => fn(signal),
|
|
52
|
+
catch: (cause) => new OrgMemoryLockCallbackError({ message: 'Org memory lock callback failed.', cause }),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function withOrgMemoryLockEffect<A, E>(
|
|
59
|
+
orgId: string,
|
|
60
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E>,
|
|
61
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError> {
|
|
13
62
|
const normalizedOrgId = orgId.trim()
|
|
14
63
|
|
|
15
64
|
if (!normalizedOrgId) {
|
|
16
|
-
|
|
65
|
+
return Effect.fail(
|
|
66
|
+
new LockAcquisitionError({
|
|
67
|
+
lockKey: `${ORG_MEMORY_LOCK_PREFIX}<missing-org-id>`,
|
|
68
|
+
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
17
71
|
}
|
|
18
72
|
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
redis: getRedisConnection(),
|
|
22
|
-
lockKey: `${ORG_MEMORY_LOCK_PREFIX}${normalizedOrgId}`,
|
|
23
|
-
lockTtlMs: ORG_MEMORY_LOCK_TTL_MS,
|
|
24
|
-
retryDelayMs: ORG_MEMORY_LOCK_RETRY_DELAY_MS,
|
|
25
|
-
refreshIntervalMs: ORG_MEMORY_LOCK_REFRESH_INTERVAL_MS,
|
|
26
|
-
waitLogIntervalMs: ORG_MEMORY_LOCK_WAIT_LOG_INTERVAL_MS,
|
|
27
|
-
maxWaitMs: ORG_MEMORY_LOCK_MAX_WAIT_MS,
|
|
28
|
-
label: 'org memory lock',
|
|
29
|
-
logger: {
|
|
30
|
-
debug: (message) => {
|
|
31
|
-
serverLogger.debug`${message}`
|
|
32
|
-
},
|
|
33
|
-
info: (message) => {
|
|
34
|
-
serverLogger.info`${message}`
|
|
35
|
-
},
|
|
36
|
-
warn: (message) => {
|
|
37
|
-
serverLogger.warn`${message}`
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
fn,
|
|
73
|
+
return Effect.sync(() => getRuntimeRedisConnection()).pipe(
|
|
74
|
+
Effect.flatMap((redis) => withLeaseLock(createOrgMemoryLockOptions(redis, normalizedOrgId), fn)),
|
|
42
75
|
)
|
|
43
76
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Clock, Deferred, Duration, Effect, Random, Schedule } from 'effect'
|
|
3
2
|
import type IORedis from 'ioredis'
|
|
4
3
|
|
|
4
|
+
import { LockAcquisitionError, LockLostError, RedisError } from '../effect/errors'
|
|
5
|
+
import { RedisServiceTag } from '../effect/services'
|
|
5
6
|
import { getErrorMessage } from '../utils/errors'
|
|
6
7
|
|
|
7
8
|
interface RedisLeaseLockLogger {
|
|
@@ -10,8 +11,8 @@ interface RedisLeaseLockLogger {
|
|
|
10
11
|
warn?: (message: string) => void
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
interface RedisLeaseLockOptions {
|
|
14
|
-
redis
|
|
14
|
+
export interface RedisLeaseLockOptions {
|
|
15
|
+
redis?: IORedis
|
|
15
16
|
lockKey: string
|
|
16
17
|
lockTtlMs: number
|
|
17
18
|
retryDelayMs?: number
|
|
@@ -47,144 +48,222 @@ function log(logger: RedisLeaseLockLogger | undefined, level: keyof RedisLeaseLo
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
const waitStart = Date.now()
|
|
55
|
-
let lastWaitLogAt = waitStart
|
|
56
|
-
|
|
57
|
-
for (;;) {
|
|
58
|
-
const result = await options.redis.set(options.lockKey, options.lockValue, 'PX', options.lockTtlMs, 'NX')
|
|
59
|
-
|
|
60
|
-
if (result === 'OK') {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
51
|
+
function toRedisError(error: unknown, message: string): RedisError {
|
|
52
|
+
return new RedisError({ message: `${message}: ${getErrorMessage(error)}`, cause: error })
|
|
53
|
+
}
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
function getNormalizedOptions(options: RedisLeaseLockOptions) {
|
|
56
|
+
const normalizedLockTtlMs = Math.max(1_000, Math.trunc(options.lockTtlMs))
|
|
57
|
+
const requestedRefreshIntervalMs = options.refreshIntervalMs ?? Math.floor(normalizedLockTtlMs / 3)
|
|
58
|
+
const normalizedRefreshIntervalMs = Math.min(
|
|
59
|
+
Math.max(250, Math.trunc(requestedRefreshIntervalMs)),
|
|
60
|
+
Math.max(250, normalizedLockTtlMs - 250),
|
|
61
|
+
)
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
return {
|
|
64
|
+
...options,
|
|
65
|
+
lockKey: options.lockKey.trim(),
|
|
66
|
+
lockTtlMs: normalizedLockTtlMs,
|
|
67
|
+
retryDelayMs: options.retryDelayMs ?? 500,
|
|
68
|
+
refreshIntervalMs: normalizedRefreshIntervalMs,
|
|
69
|
+
waitLogIntervalMs: options.waitLogIntervalMs ?? 30_000,
|
|
70
|
+
maxWaitMs: options.maxWaitMs ?? 45_000,
|
|
71
|
+
acquiredWaitInfoThresholdMs: options.acquiredWaitInfoThresholdMs ?? 1_000,
|
|
72
|
+
heldInfoThresholdMs: options.heldInfoThresholdMs ?? 5_000,
|
|
73
|
+
label: options.label ?? 'redis lease lock',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
function resolveRedisConnection(options: RedisLeaseLockOptions): Effect.Effect<IORedis, RedisError, RedisServiceTag> {
|
|
78
|
+
if (options.redis) {
|
|
79
|
+
return Effect.succeed(options.redis)
|
|
76
80
|
}
|
|
81
|
+
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const redisService = yield* RedisServiceTag
|
|
84
|
+
return redisService.getConnection()
|
|
85
|
+
})
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
function acquireLock(
|
|
89
|
+
redis: IORedis,
|
|
90
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
91
|
+
lockValue: string,
|
|
92
|
+
): Effect.Effect<void, LockAcquisitionError | RedisError> {
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
const waitStart = yield* Clock.currentTimeMillis
|
|
95
|
+
let lastWaitLogAt = waitStart
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
const tryOnce = Effect.gen(function* () {
|
|
98
|
+
const result = yield* Effect.tryPromise({
|
|
99
|
+
try: () => redis.set(options.lockKey, lockValue, 'PX', options.lockTtlMs, 'NX'),
|
|
100
|
+
catch: (error) => toRedisError(error, `Failed to acquire ${options.label} (${options.lockKey})`),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (result === 'OK') {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const now = yield* Clock.currentTimeMillis
|
|
108
|
+
const waitedMs = now - waitStart
|
|
109
|
+
if (now - lastWaitLogAt >= options.waitLogIntervalMs) {
|
|
110
|
+
log(options.logger, 'info', `Waiting for ${options.label} (${options.lockKey}) waitedMs=${waitedMs}`)
|
|
111
|
+
lastWaitLogAt = now
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return yield* new LockAcquisitionError({ lockKey: options.lockKey, maxWaitMs: options.maxWaitMs })
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return yield* Effect.retry(tryOnce, {
|
|
118
|
+
times: Math.max(0, Math.ceil(options.maxWaitMs / options.retryDelayMs) - 1),
|
|
119
|
+
schedule: Schedule.fixed(Duration.millis(options.retryDelayMs)),
|
|
120
|
+
}).pipe(
|
|
121
|
+
Effect.asVoid,
|
|
122
|
+
Effect.catchTag('LockAcquisitionError', () =>
|
|
123
|
+
Effect.fail(new LockAcquisitionError({ lockKey: options.lockKey, maxWaitMs: options.maxWaitMs })),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
})
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
function refreshLock(
|
|
130
|
+
redis: IORedis,
|
|
131
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
132
|
+
lockValue: string,
|
|
133
|
+
): Effect.Effect<void, LockLostError | RedisError> {
|
|
134
|
+
return Effect.tryPromise({
|
|
135
|
+
try: () => redis.eval(REFRESH_LOCK_SCRIPT, 1, options.lockKey, lockValue, options.lockTtlMs.toString()),
|
|
136
|
+
catch: (error) => toRedisError(error, `Failed to refresh ${options.label} (${options.lockKey})`),
|
|
137
|
+
}).pipe(
|
|
138
|
+
Effect.flatMap((refreshed) => {
|
|
139
|
+
const refreshedCount = typeof refreshed === 'number' ? refreshed : Number(refreshed)
|
|
140
|
+
if (refreshedCount === 1) {
|
|
141
|
+
return Effect.void
|
|
142
|
+
}
|
|
143
|
+
return Effect.fail(new LockLostError({ lockKey: options.lockKey }))
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
function releaseLock(
|
|
149
|
+
redis: IORedis,
|
|
150
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
151
|
+
lockValue: string,
|
|
152
|
+
): Effect.Effect<void, RedisError> {
|
|
153
|
+
return Effect.asVoid(
|
|
154
|
+
Effect.tryPromise({
|
|
155
|
+
try: () => redis.eval(RELEASE_LOCK_SCRIPT, 1, options.lockKey, lockValue),
|
|
156
|
+
catch: (error) => toRedisError(error, `Failed to release ${options.label} (${options.lockKey})`),
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
102
159
|
}
|
|
103
160
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ac.abort(new LeaseLockLostError(message))
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
}, options.refreshIntervalMs)
|
|
161
|
+
function startRefreshFiber(
|
|
162
|
+
redis: IORedis,
|
|
163
|
+
options: ReturnType<typeof getNormalizedOptions>,
|
|
164
|
+
lockValue: string,
|
|
165
|
+
abortController: AbortController,
|
|
166
|
+
lockLost: Deferred.Deferred<never, LockLostError | RedisError>,
|
|
167
|
+
) {
|
|
168
|
+
const handleLockLoss = (error: LockLostError | RedisError) =>
|
|
169
|
+
Deferred.fail(lockLost, error).pipe(
|
|
170
|
+
Effect.andThen(
|
|
171
|
+
Effect.sync(() => {
|
|
172
|
+
const message =
|
|
173
|
+
error._tag === 'LockLostError'
|
|
174
|
+
? `${options.label} refresh was rejected for key ${options.lockKey}`
|
|
175
|
+
: error.message
|
|
123
176
|
|
|
124
|
-
|
|
177
|
+
log(options.logger, 'warn', message)
|
|
178
|
+
if (!abortController.signal.aborted) {
|
|
179
|
+
abortController.abort(error)
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
Effect.andThen(Effect.fail(error)),
|
|
184
|
+
)
|
|
125
185
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
const refreshProgram = Effect.gen(function* () {
|
|
187
|
+
for (;;) {
|
|
188
|
+
yield* Effect.sleep(Duration.millis(options.refreshIntervalMs))
|
|
189
|
+
|
|
190
|
+
yield* refreshLock(redis, options, lockValue).pipe(
|
|
191
|
+
Effect.catchTag('LockLostError', handleLockLoss),
|
|
192
|
+
Effect.catchTag('RedisError', handleLockLoss),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return Effect.forkScoped(refreshProgram).pipe(Effect.asVoid)
|
|
130
198
|
}
|
|
131
199
|
|
|
132
|
-
export
|
|
200
|
+
export function withLeaseLock<A, E, R>(
|
|
201
|
+
options: RedisLeaseLockOptions & { redis: IORedis },
|
|
202
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E, R>,
|
|
203
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError, R>
|
|
204
|
+
export function withLeaseLock<A, E, R>(
|
|
133
205
|
options: RedisLeaseLockOptions,
|
|
134
|
-
fn: (signal: AbortSignal) =>
|
|
135
|
-
):
|
|
136
|
-
const
|
|
137
|
-
if (!lockKey) {
|
|
138
|
-
throw new Error('Redis lease lock requires a non-empty lock key')
|
|
139
|
-
}
|
|
206
|
+
fn: (signal: AbortSignal) => Effect.Effect<A, E, R>,
|
|
207
|
+
): Effect.Effect<A, E | LockAcquisitionError | LockLostError | RedisError, RedisServiceTag | R> {
|
|
208
|
+
const normalized = getNormalizedOptions(options)
|
|
140
209
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const acquiredWaitInfoThresholdMs = options.acquiredWaitInfoThresholdMs ?? 1_000
|
|
146
|
-
const heldInfoThresholdMs = options.heldInfoThresholdMs ?? 5_000
|
|
147
|
-
const label = options.label ?? 'redis lease lock'
|
|
148
|
-
|
|
149
|
-
const lockValue = crypto.randomUUID()
|
|
150
|
-
const waitStart = Date.now()
|
|
151
|
-
await acquireLeaseLock({ ...options, lockKey, lockValue, label, retryDelayMs, waitLogIntervalMs, maxWaitMs })
|
|
152
|
-
const waitedMs = Date.now() - waitStart
|
|
153
|
-
if (waitedMs >= acquiredWaitInfoThresholdMs) {
|
|
154
|
-
log(options.logger, 'info', `Acquired ${label} (${lockKey}) after waiting waitedMs=${waitedMs}`)
|
|
155
|
-
}
|
|
210
|
+
return Effect.gen(function* () {
|
|
211
|
+
if (!normalized.lockKey) {
|
|
212
|
+
return yield* new LockAcquisitionError({ lockKey: '', maxWaitMs: normalized.maxWaitMs })
|
|
213
|
+
}
|
|
156
214
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
215
|
+
const redis = yield* resolveRedisConnection(options)
|
|
216
|
+
const lockValue = yield* Random.nextUUIDv4
|
|
217
|
+
const waitStart = yield* Clock.currentTimeMillis
|
|
160
218
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
() => {
|
|
170
|
-
reject(ac.signal.reason as Error)
|
|
171
|
-
},
|
|
172
|
-
{ once: true },
|
|
219
|
+
yield* acquireLock(redis, normalized, lockValue)
|
|
220
|
+
|
|
221
|
+
const waitedMs = (yield* Clock.currentTimeMillis) - waitStart
|
|
222
|
+
if (waitedMs >= normalized.acquiredWaitInfoThresholdMs) {
|
|
223
|
+
log(
|
|
224
|
+
normalized.logger,
|
|
225
|
+
'info',
|
|
226
|
+
`Acquired ${normalized.label} (${normalized.lockKey}) after waiting waitedMs=${waitedMs}`,
|
|
173
227
|
)
|
|
174
|
-
})
|
|
175
|
-
return await Promise.race([fn(ac.signal), abortPromise])
|
|
176
|
-
} finally {
|
|
177
|
-
stopRefreshLoop()
|
|
178
|
-
const heldMs = Date.now() - holdStart
|
|
179
|
-
if (!ac.signal.aborted) {
|
|
180
|
-
await releaseLeaseLock({ ...options, lockKey, lockValue }).catch((error: unknown) => {
|
|
181
|
-
log(options.logger, 'warn', `Failed to release ${label} (${lockKey}): ${getErrorMessage(error)}`)
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
if (heldMs >= heldInfoThresholdMs) {
|
|
185
|
-
log(options.logger, 'info', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
|
|
186
|
-
} else {
|
|
187
|
-
log(options.logger, 'debug', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
|
|
188
228
|
}
|
|
189
|
-
|
|
229
|
+
|
|
230
|
+
const abortController = new AbortController()
|
|
231
|
+
const holdStart = yield* Clock.currentTimeMillis
|
|
232
|
+
const lockLost = yield* Deferred.make<never, LockLostError | RedisError>()
|
|
233
|
+
|
|
234
|
+
const cleanup = Effect.sync(() => {
|
|
235
|
+
if (!abortController.signal.aborted) {
|
|
236
|
+
abortController.abort()
|
|
237
|
+
}
|
|
238
|
+
}).pipe(
|
|
239
|
+
Effect.andThen(
|
|
240
|
+
releaseLock(redis, normalized, lockValue).pipe(
|
|
241
|
+
Effect.catch((error) =>
|
|
242
|
+
Effect.sync(() => {
|
|
243
|
+
log(normalized.logger, 'warn', error.message)
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
Effect.andThen(
|
|
249
|
+
Effect.gen(function* () {
|
|
250
|
+
const heldMs = (yield* Clock.currentTimeMillis) - holdStart
|
|
251
|
+
if (heldMs >= normalized.heldInfoThresholdMs) {
|
|
252
|
+
log(normalized.logger, 'info', `Released ${normalized.label} (${normalized.lockKey}) heldMs=${heldMs}`)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
log(normalized.logger, 'debug', `Released ${normalized.label} (${normalized.lockKey}) heldMs=${heldMs}`)
|
|
257
|
+
}),
|
|
258
|
+
),
|
|
259
|
+
Effect.ignore,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return yield* Effect.scoped(
|
|
263
|
+
startRefreshFiber(redis, normalized, lockValue, abortController, lockLost).pipe(
|
|
264
|
+
Effect.andThen(fn(abortController.signal).pipe(Effect.raceFirst(Deferred.await(lockLost)))),
|
|
265
|
+
Effect.ensuring(cleanup),
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
})
|
|
190
269
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import type IORedis from 'ioredis'
|
|
3
|
+
|
|
4
|
+
import { getCurrentRuntime } from '../effect/runtime-ref'
|
|
5
|
+
import { RedisServiceTag } from '../effect/services'
|
|
6
|
+
|
|
7
|
+
export function getRuntimeRedisConnection(): IORedis {
|
|
8
|
+
const redis = getCurrentRuntime().runSync(Effect.service(RedisServiceTag))
|
|
9
|
+
return redis.getConnection()
|
|
10
|
+
}
|