fix(lint): resolve restricted import and console.log warnings
Change circuit-breaker import in resilient-search.repository.ts to use @modules/shared barrel export instead of deep path, fixing no-restricted-imports error. Replace console.log with console.warn in encrypt-existing-kyc.ts script to satisfy no-console rule. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectMetric } from '@willsoto/nestjs-prometheus';
|
||||
import { type Counter } from 'prom-client';
|
||||
import {
|
||||
CircuitBreaker,
|
||||
CircuitOpenError,
|
||||
type CircuitState,
|
||||
type LoggerService,
|
||||
} from '@modules/shared';
|
||||
import {
|
||||
type ISearchRepository,
|
||||
type ListingDocument,
|
||||
type SearchParams,
|
||||
type SearchResult,
|
||||
} from '../../domain/repositories/search.repository';
|
||||
import { type PostgresSearchRepository } from './postgres-search.repository';
|
||||
import { type TypesenseSearchRepository } from './typesense-search.repository';
|
||||
|
||||
export const SEARCH_DEGRADATION_TOTAL = 'search_degradation_total';
|
||||
|
||||
/**
|
||||
* Resilient search repository that wraps Typesense with a circuit breaker
|
||||
* and automatically falls back to PostgreSQL full-text search when
|
||||
* Typesense is unavailable.
|
||||
*
|
||||
* - Normal operation: all searches go through Typesense.
|
||||
* - Typesense failures trip the circuit breaker after 3 consecutive errors.
|
||||
* - While the circuit is OPEN, searches transparently fall back to PostgreSQL.
|
||||
* - After 30 seconds the circuit enters HALF_OPEN and probes Typesense with
|
||||
* the next search request.
|
||||
* - Indexing operations are always forwarded to Typesense (best-effort)
|
||||
* plus are inherently no-ops on the PG side.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ResilientSearchRepository implements ISearchRepository {
|
||||
private readonly breaker: CircuitBreaker;
|
||||
|
||||
constructor(
|
||||
private readonly typesense: TypesenseSearchRepository,
|
||||
private readonly postgres: PostgresSearchRepository,
|
||||
private readonly logger: LoggerService,
|
||||
@InjectMetric(SEARCH_DEGRADATION_TOTAL)
|
||||
private readonly degradationCounter: Counter,
|
||||
) {
|
||||
this.breaker = new CircuitBreaker({
|
||||
name: 'typesense',
|
||||
failureThreshold: 3,
|
||||
resetTimeMs: 30_000,
|
||||
onStateChange: (from: CircuitState, to: CircuitState, name: string) => {
|
||||
this.logger.warn(
|
||||
`Circuit breaker [${name}] transitioned: ${from} → ${to}`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
if (to === 'OPEN') {
|
||||
this.degradationCounter.inc({ service: 'typesense', event: 'circuit_opened' });
|
||||
} else if (to === 'CLOSED') {
|
||||
this.logger.log(
|
||||
`Circuit breaker [${name}] recovered — Typesense is healthy again`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async search(params: SearchParams): Promise<SearchResult> {
|
||||
try {
|
||||
return await this.breaker.exec(() => this.typesense.search(params));
|
||||
} catch (err) {
|
||||
if (err instanceof CircuitOpenError) {
|
||||
this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
|
||||
this.logger.warn(
|
||||
'Typesense circuit OPEN — falling back to PostgreSQL search',
|
||||
'ResilientSearch',
|
||||
);
|
||||
} else {
|
||||
this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
|
||||
this.logger.warn(
|
||||
`Typesense search failed (${(err as Error).message}) — falling back to PostgreSQL`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
|
||||
return this.postgres.search(params);
|
||||
}
|
||||
}
|
||||
|
||||
async indexDocument(doc: ListingDocument): Promise<void> {
|
||||
try {
|
||||
await this.typesense.indexDocument(doc);
|
||||
} catch (err) {
|
||||
this.breaker.recordFailure();
|
||||
this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
|
||||
this.logger.warn(
|
||||
`Typesense indexDocument failed: ${(err as Error).message}`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async indexDocuments(docs: ListingDocument[]): Promise<void> {
|
||||
try {
|
||||
await this.typesense.indexDocuments(docs);
|
||||
} catch (err) {
|
||||
this.breaker.recordFailure();
|
||||
this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
|
||||
this.logger.warn(
|
||||
`Typesense indexDocuments failed (${docs.length} docs): ${(err as Error).message}`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeDocument(id: string): Promise<void> {
|
||||
try {
|
||||
await this.typesense.removeDocument(id);
|
||||
} catch (err) {
|
||||
this.breaker.recordFailure();
|
||||
this.logger.warn(
|
||||
`Typesense removeDocument failed for ${id}: ${(err as Error).message}`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async ensureCollection(): Promise<void> {
|
||||
try {
|
||||
await this.typesense.ensureCollection();
|
||||
this.breaker.recordSuccess();
|
||||
} catch (err) {
|
||||
this.breaker.recordFailure();
|
||||
this.logger.warn(
|
||||
`Typesense ensureCollection failed: ${(err as Error).message} — PostgreSQL fallback is available`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async dropCollection(): Promise<void> {
|
||||
try {
|
||||
await this.typesense.dropCollection();
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Typesense dropCollection failed: ${(err as Error).message}`,
|
||||
'ResilientSearch',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user