187 lines
5.6 KiB
TypeScript
187 lines
5.6 KiB
TypeScript
import { getContainer } from '@bytelyst/cosmos';
|
|
import { config } from '../config/index.js';
|
|
import logger from '../utils/logger.js';
|
|
|
|
const CONTAINER_NAME = 'runtime_locks';
|
|
|
|
type LockScope = 'entry' | 'reconciliation';
|
|
|
|
interface RuntimeLockDocument {
|
|
id: string;
|
|
productId: string;
|
|
scope: LockScope;
|
|
profileId: string;
|
|
symbol?: string;
|
|
owner: string;
|
|
expiresAt: number;
|
|
updatedAt: string;
|
|
ttl: number;
|
|
}
|
|
|
|
function isCosmosConfigured(): boolean {
|
|
return Boolean(config.COSMOS_ENDPOINT && config.COSMOS_KEY);
|
|
}
|
|
|
|
function buildEntryLockId(profileId: string, symbol?: string): string {
|
|
return `entry:${profileId}:${String(symbol || '').trim()}`;
|
|
}
|
|
|
|
function buildReconciliationLockId(profileId: string): string {
|
|
return `reconciliation:${profileId}`;
|
|
}
|
|
|
|
async function readLock(id: string): Promise<RuntimeLockDocument | null> {
|
|
const container = getContainer(CONTAINER_NAME);
|
|
try {
|
|
const { resource } = await container.item(id, config.PRODUCT_ID).read<RuntimeLockDocument>();
|
|
return resource || null;
|
|
} catch (error) {
|
|
const code = (error as { code?: number })?.code;
|
|
if (code === 404) return null;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function writeLock(doc: RuntimeLockDocument): Promise<boolean> {
|
|
const container = getContainer(CONTAINER_NAME);
|
|
await container.items.upsert(doc);
|
|
return true;
|
|
}
|
|
|
|
async function deleteLock(id: string): Promise<boolean> {
|
|
const container = getContainer(CONTAINER_NAME);
|
|
try {
|
|
await container.item(id, config.PRODUCT_ID).delete();
|
|
return true;
|
|
} catch (error) {
|
|
const code = (error as { code?: number })?.code;
|
|
if (code === 404) return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function isActive(doc: RuntimeLockDocument | null | undefined): boolean {
|
|
return Boolean(doc && Number(doc.expiresAt) > Date.now());
|
|
}
|
|
|
|
export async function tryAcquireEntryLock(
|
|
profileId: string,
|
|
symbol: string | undefined,
|
|
owner: string,
|
|
ttlSeconds: number
|
|
): Promise<boolean> {
|
|
if (!profileId || !owner) return false;
|
|
if (!isCosmosConfigured()) return false;
|
|
|
|
try {
|
|
const id = buildEntryLockId(profileId, symbol);
|
|
const existing = await readLock(id);
|
|
if (isActive(existing) && existing?.owner !== owner) {
|
|
return false;
|
|
}
|
|
|
|
await writeLock({
|
|
id,
|
|
productId: config.PRODUCT_ID,
|
|
scope: 'entry',
|
|
profileId,
|
|
symbol: String(symbol || '').trim() || undefined,
|
|
owner,
|
|
expiresAt: Date.now() + Math.max(1, ttlSeconds) * 1000,
|
|
updatedAt: new Date().toISOString(),
|
|
ttl: Math.max(1, ttlSeconds)
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
logger.warn(`[DistributedLockRepo] Cosmos entry-lock acquire failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function releaseEntryLock(
|
|
profileId: string,
|
|
symbol: string | undefined,
|
|
owner: string
|
|
): Promise<boolean> {
|
|
if (!profileId || !owner) return false;
|
|
if (!isCosmosConfigured()) return false;
|
|
|
|
try {
|
|
const id = buildEntryLockId(profileId, symbol);
|
|
const existing = await readLock(id);
|
|
if (!existing) return false;
|
|
if (existing.owner !== owner) return false;
|
|
return await deleteLock(id);
|
|
} catch (error) {
|
|
logger.warn(`[DistributedLockRepo] Cosmos entry-lock release failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function tryAcquireReconciliationLock(
|
|
profileId: string,
|
|
owner: string,
|
|
ttlSeconds: number
|
|
): Promise<boolean> {
|
|
if (!profileId || !owner) return false;
|
|
if (!isCosmosConfigured()) return false;
|
|
|
|
try {
|
|
const id = buildReconciliationLockId(profileId);
|
|
const existing = await readLock(id);
|
|
if (isActive(existing) && existing?.owner !== owner) {
|
|
return false;
|
|
}
|
|
|
|
await writeLock({
|
|
id,
|
|
productId: config.PRODUCT_ID,
|
|
scope: 'reconciliation',
|
|
profileId,
|
|
owner,
|
|
expiresAt: Date.now() + Math.max(1, ttlSeconds) * 1000,
|
|
updatedAt: new Date().toISOString(),
|
|
ttl: Math.max(1, ttlSeconds)
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
logger.warn(`[DistributedLockRepo] Cosmos reconciliation-lock acquire failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function releaseReconciliationLock(
|
|
profileId: string,
|
|
owner: string
|
|
): Promise<boolean> {
|
|
if (!profileId || !owner) return false;
|
|
if (!isCosmosConfigured()) return false;
|
|
|
|
try {
|
|
const id = buildReconciliationLockId(profileId);
|
|
const existing = await readLock(id);
|
|
if (!existing) return false;
|
|
if (existing.owner !== owner) return false;
|
|
return await deleteLock(id);
|
|
} catch (error) {
|
|
logger.warn(`[DistributedLockRepo] Cosmos reconciliation-lock release failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function isEntryLockActive(
|
|
profileId: string,
|
|
symbol: string | undefined
|
|
): Promise<boolean> {
|
|
if (!profileId) return false;
|
|
if (!isCosmosConfigured()) return false;
|
|
|
|
try {
|
|
const existing = await readLock(buildEntryLockId(profileId, symbol));
|
|
return isActive(existing);
|
|
} catch (error) {
|
|
logger.warn(`[DistributedLockRepo] Cosmos entry-lock status failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
return true;
|
|
}
|
|
}
|