From 65a3203bf959240fffb80b9515f8e45cf2febb90 Mon Sep 17 00:00:00 2001 From: Efe Baran Durmaz Date: Wed, 6 May 2026 21:33:17 +0300 Subject: [PATCH] fix(trust-graph): count discovery 5xx failures --- .../trust-graph/src/services/trust-service.ts | 17 +++++++------ .../trust-graph/test/trust-service.test.ts | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/services/trust-graph/src/services/trust-service.ts b/services/trust-graph/src/services/trust-service.ts index 62b2dc4..1e63fe5 100644 --- a/services/trust-graph/src/services/trust-service.ts +++ b/services/trust-graph/src/services/trust-service.ts @@ -314,16 +314,11 @@ export class TrustService { // Reset circuit breaker on success this.circuitBreaker.failureCount = 0 } else if (response.status >= 500) { - throw new TrustError(`Discovery service unavailable while resolving identity: ${did}`) + this.recordDiscoveryFailure(did) } } catch (error) { if (error instanceof TrustError) throw error - // Discovery service unavailable — increment circuit breaker - this.circuitBreaker.failureCount++ - if (this.circuitBreaker.failureCount >= CIRCUIT_BREAKER_THRESHOLD) { - this.circuitBreaker.openUntil = Date.now() + CIRCUIT_BREAKER_RESET_MS - } - throw new TrustError(`Discovery service unavailable while resolving identity: ${did}`) + this.recordDiscoveryFailure(did) } if (!identity || !identity.publicKey) { @@ -517,6 +512,14 @@ export class TrustService { return new Uint8Array(publicKey) } + + private recordDiscoveryFailure(did: string): never { + this.circuitBreaker.failureCount++ + if (this.circuitBreaker.failureCount >= CIRCUIT_BREAKER_THRESHOLD) { + this.circuitBreaker.openUntil = Date.now() + CIRCUIT_BREAKER_RESET_MS + } + throw new TrustError(`Discovery service unavailable while resolving identity: ${did}`) + } } function isRevocationRecord(record: unknown): record is RevocationRecord { diff --git a/services/trust-graph/test/trust-service.test.ts b/services/trust-graph/test/trust-service.test.ts index c112aad..20cb7b9 100644 --- a/services/trust-graph/test/trust-service.test.ts +++ b/services/trust-graph/test/trust-service.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { TrustService } from '../src/services/trust-service.js' import type { CreateTrustRequest } from '../src/types.js' import { @@ -39,6 +39,10 @@ describe('TrustService', () => { } }) + afterEach(() => { + vi.unstubAllGlobals() + }) + function mockIdentity(publicKey: Uint8Array) { mockDb.select = vi.fn(() => ({ from: vi.fn(() => ({ @@ -212,6 +216,25 @@ describe('TrustService', () => { }) describe('getScore', () => { + it('opens the discovery circuit breaker after repeated 5xx responses', async () => { + service = new TrustService('http://discovery.test') + const fetchMock = vi.fn(() => Promise.resolve(new Response('unavailable', { status: 503 }))) + vi.stubGlobal('fetch', fetchMock) + + for (let attempt = 0; attempt < 5; attempt++) { + await expect(service.getScore(mockDb, `did:fides:missing-${attempt}`)).rejects.toThrow( + 'Discovery service unavailable' + ) + } + + expect(fetchMock).toHaveBeenCalledTimes(5) + + await expect(service.getScore(mockDb, 'did:fides:circuit-open')).rejects.toThrow( + 'Discovery service unavailable' + ) + expect(fetchMock).toHaveBeenCalledTimes(5) + }) + it('should return cached score if valid', async () => { const now = new Date() const recentCompute = new Date(now.getTime() - 1800000) // 30 min ago