Skip to content

Commit eb401c2

Browse files
authored
Merge pull request #196 from Oluwatomilola/circuit
feat: Fix for Circuit #133
2 parents 9b380eb + 8d27cf6 commit eb401c2

4 files changed

Lines changed: 56 additions & 23 deletions

File tree

src/stellar/client.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ export class StellarClient {
3333
/** Load account record from Horizon. */
3434
async getAccount(publicKey: string): Promise<StellarSdk.Horizon.AccountResponse> {
3535
try {
36-
return await circuitBreakerExecute(() =>
37-
stellarRetry.execute(() =>
38-
withTimeout(this.horizon.loadAccount(publicKey), READ_TIMEOUT_MS)
39-
)
36+
return await circuitBreakerExecute(
37+
() =>
38+
stellarRetry.execute(() =>
39+
withTimeout(this.horizon.loadAccount(publicKey), READ_TIMEOUT_MS)
40+
),
41+
"read"
4042
);
4143
} catch (err) {
4244
logger.error({ err, publicKey }, "Failed to load Stellar account");
@@ -49,10 +51,12 @@ export class StellarClient {
4951
txEnvelope: StellarSdk.Transaction | StellarSdk.FeeBumpTransaction
5052
): Promise<StellarSdk.Horizon.HorizonApi.SubmitTransactionResponse> {
5153
try {
52-
const result = await circuitBreakerExecute(() =>
53-
stellarRetry.execute(() =>
54-
withTimeout(this.horizon.submitTransaction(txEnvelope), WRITE_TIMEOUT_MS)
55-
)
54+
const result = await circuitBreakerExecute(
55+
() =>
56+
stellarRetry.execute(() =>
57+
withTimeout(this.horizon.submitTransaction(txEnvelope), WRITE_TIMEOUT_MS)
58+
),
59+
"write"
5660
);
5761
logger.info({ hash: result.hash }, "Transaction submitted successfully");
5862
return result;

src/stellar/resilience.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,31 @@ import {
1010

1111
export const stellarRetry = createTransientRetryPolicy("Stellar");
1212

13-
const stellarBreaker = createCircuitBreaker({ label: "Stellar" });
13+
export type StellarOperation = "read" | "write";
1414

15-
export function getCircuitState(): CircuitState {
16-
return stellarBreaker.getState();
15+
const stellarReadBreaker = createCircuitBreaker({ label: "StellarRead" });
16+
const stellarWriteBreaker = createCircuitBreaker({ label: "StellarWrite" });
17+
18+
function getBreaker(operation: StellarOperation = "read") {
19+
return operation === "write" ? stellarWriteBreaker : stellarReadBreaker;
20+
}
21+
22+
export function getCircuitState(operation: StellarOperation = "read"): CircuitState {
23+
return getBreaker(operation).getState();
1724
}
1825

19-
export function resetCircuitBreaker(): void {
20-
stellarBreaker.reset();
26+
export function resetCircuitBreaker(operation?: StellarOperation): void {
27+
if (operation) {
28+
getBreaker(operation).reset();
29+
return;
30+
}
31+
32+
stellarReadBreaker.reset();
33+
stellarWriteBreaker.reset();
2134
}
2235

23-
export function circuitBreakerExecute<T>(fn: () => Promise<T>): Promise<T> {
24-
return stellarBreaker.execute(fn);
36+
export function circuitBreakerExecute<T>(fn: () => Promise<T>, operation: StellarOperation = "read"): Promise<T> {
37+
return getBreaker(operation).execute(fn);
2538
}
2639

2740
export { withTimeout, isCircuitBreakerError, CircuitState, CircuitBreakerOpenError, TimeoutError };

tests/unit/services/resilience.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ describe("Stellar Resilience", () => {
9696
vi.useRealTimers();
9797
}
9898
});
99+
100+
it("should isolate breaker state between read and write operations", async () => {
101+
const readFailure = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
102+
const writeSuccess = vi.fn().mockResolvedValue("write-ok");
103+
104+
for (let i = 0; i < 5; i++) {
105+
try {
106+
await circuitBreakerExecute(readFailure, "read");
107+
} catch {
108+
// expected
109+
}
110+
}
111+
112+
await expect(circuitBreakerExecute(writeSuccess, "write")).resolves.toBe("write-ok");
113+
});
99114
});
100115

101116
describe("Timeout", () => {

tests/unit/stellar/client.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
22

3+
const { mockHorizon, mockSoroban } = vi.hoisted(() => ({
4+
mockHorizon: {
5+
loadAccount: vi.fn(),
6+
submitTransaction: vi.fn(),
7+
},
8+
mockSoroban: {},
9+
}));
10+
311
vi.mock("../../../src/config/stellar.js", () => ({
412
getHorizonServer: vi.fn(() => mockHorizon),
513
getSorobanServer: vi.fn(() => mockSoroban),
@@ -14,17 +22,10 @@ vi.mock("../../../src/stellar/resilience.js", () => ({
1422
stellarRetry: {
1523
execute: vi.fn((fn) => fn()),
1624
},
17-
circuitBreakerExecute: vi.fn((fn) => fn()),
25+
circuitBreakerExecute: vi.fn((fn, _operation) => fn()),
1826
withTimeout: vi.fn((promise) => promise),
1927
}));
2028

21-
const mockHorizon = {
22-
loadAccount: vi.fn(),
23-
submitTransaction: vi.fn(),
24-
};
25-
26-
const mockSoroban = {};
27-
2829
import { StellarClient } from "../../../src/stellar/client.js";
2930
import { StellarError } from "../../../src/utils/errors.js";
3031

0 commit comments

Comments
 (0)