refactor!: simplify runtime coordination #7
Backend verify / commits (pull_request) Successful in 29s
Backend verify / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 9m36s

Keep financial ownership, atomic publication and refresh CAS.
Failed refresh leaves cookies untouched; logout still clears them.

Remove full-history candle prefetch and special metadata retries.
Use native Pino request context instead of parallel middleware.

BREAKING CHANGE: remove investment stateVersion, queued sync state,
and tbank-sync status progress/resources. Consumers must observe
resources through sync=observe with the updated frontend contract.
This commit is contained in:
2026-10-03 11:38:08 +03:00
parent 0f27da9fd1
commit d698ed9425
48 changed files with 185 additions and 925 deletions
+12 -48
View File
@@ -176,46 +176,30 @@ describe('TbankClient metadata retries', () => {
status.DEADLINE_EXCEEDED,
status.RESOURCE_EXHAUSTED,
])(
'recovers metadata after transient gRPC status %s with 2s/4s pauses',
'recovers metadata after one retry for transient gRPC status %s',
async (code) => {
requestRpc
.mockRejectedValueOnce({ code })
.mockRejectedValueOnce({ code })
.mockResolvedValueOnce({ instrument });
const result = client.getInstrumentByUid('token', 'uid');
await jest.advanceTimersByTimeAsync(1999);
await jest.advanceTimersByTimeAsync(999);
expect(requestRpc).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(1);
expect(requestRpc).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(3999);
expect(requestRpc).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(1);
await expect(result).resolves.toEqual(instrument);
expect(queue.run).toHaveBeenCalledTimes(3);
expect(queue.run).toHaveBeenCalledTimes(2);
expect(jest.getTimerCount()).toBe(0);
},
);
it('rechecks a full queue before admitting a metadata retry', async () => {
it('rejects metadata immediately when the queue is full', async () => {
queue.size = 50;
requestRpc.mockResolvedValue({ instrument });
const result = client.getInstrumentByFigi('token', 'figi');
await jest.advanceTimersByTimeAsync(1999);
await expect(client.getInstrumentByFigi('token', 'figi')).rejects.toBeInstanceOf(ServiceUnavailableException);
expect(queue.run).not.toHaveBeenCalled();
queue.size = 49;
await jest.advanceTimersByTimeAsync(1);
await expect(result).resolves.toEqual(instrument);
expect(queue.run).toHaveBeenCalledTimes(1);
expect(logger.event).toHaveBeenCalledWith(
'warn',
'tbank.rpc.retry',
expect.objectContaining({ code: 'TBANK_UNAVAILABLE', attempts: 1 }),
);
expect(logger.event.mock.calls[0][2]).not.toHaveProperty('grpcCode');
expect(jest.getTimerCount()).toBe(0);
});
it('does not bypass queue admission after an upstream failure', async () => {
@@ -223,42 +207,22 @@ describe('TbankClient metadata retries', () => {
.mockRejectedValueOnce({ code: status.UNAVAILABLE })
.mockResolvedValueOnce({ instrument });
const result = client.getInstrumentByUid('token', 'uid');
const result = client.getInstrumentByUid('token', 'uid').catch((error: unknown) => error);
queue.size = 50;
await jest.advanceTimersByTimeAsync(2000);
await jest.advanceTimersByTimeAsync(1000);
expect(queue.run).toHaveBeenCalledTimes(1);
queue.size = 0;
await jest.advanceTimersByTimeAsync(4000);
await expect(result).resolves.toEqual(instrument);
expect(queue.run).toHaveBeenCalledTimes(2);
});
it('stops retrying a saturated queue after three admission attempts', async () => {
queue.size = 50;
await Promise.all([
expect(client.getInstrumentByUid('token', 'uid')).rejects.toBeInstanceOf(ServiceUnavailableException),
jest.advanceTimersByTimeAsync(6000),
]);
expect(queue.run).not.toHaveBeenCalled();
expect(logger.event).toHaveBeenCalledTimes(3);
expect(logger.event).toHaveBeenLastCalledWith(
'error',
'tbank.rpc.failed',
expect.objectContaining({ code: 'TBANK_UNAVAILABLE', attempts: 3 }),
);
expect(await result).toBeInstanceOf(ServiceUnavailableException);
expect(jest.getTimerCount()).toBe(0);
});
it('stops retrying an unavailable upstream after three RPCs', async () => {
it('stops retrying an unavailable upstream after two RPCs', async () => {
requestRpc.mockRejectedValue({ code: status.UNAVAILABLE });
await Promise.all([
expect(client.getInstrumentByUid('token', 'uid')).rejects.toBeInstanceOf(ServiceUnavailableException),
jest.advanceTimersByTimeAsync(6000),
jest.advanceTimersByTimeAsync(1000),
]);
expect(queue.run).toHaveBeenCalledTimes(3);
expect(queue.run).toHaveBeenCalledTimes(2);
expect(jest.getTimerCount()).toBe(0);
});