Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: retry using replica time when receiving ingress expiry error #934

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions e2e/node/basic/mainnet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ test('it should throw an error when the clock is out of sync during a query', as
const error = err as ReplicaTimeError;
// use the replica time to sync the agent
error.agent.replicaTime = error.replicaTime;
error.agent.overrideSystemTime = true;
}
}
// retry the call
Expand Down Expand Up @@ -229,6 +230,7 @@ test('it should throw an error when the clock is out of sync during an update',
const error = err as ReplicaTimeError;
// use the replica time to sync the agent
error.agent.replicaTime = error.replicaTime;
error.agent.overrideSystemTime = true;
// retry the call
const result = await actor.whoami();
expect(Principal.from(result)).toBeInstanceOf(Principal);
Expand Down
30 changes: 22 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/agent/src/__snapshots__/actor.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ exports[`makeActor should enrich actor interface with httpDetails 2`] = `
"__principal__": "2chl6-4hpzw-vqaaa-aaaaa-c",
},
"ingress_expiry": Expiry {
"_value": 1200000000000n,
"_value": 1260000000000n,
},
"method_name": "greet_update",
"nonce": Uint8Array [],
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ describe('makeActor', () => {
"__principal__": "2chl6-4hpzw-vqaaa-aaaaa-c",
},
"ingress_expiry": Expiry {
"_value": 1200000000000n,
"_value": 1260000000000n,
},
"method_name": "greet",
"request_type": "query",
Expand Down
2 changes: 0 additions & 2 deletions packages/agent/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,6 @@ function _createActorMethod(
? (agent as HttpAgent).replicaTime
: undefined;

certTime;

if (response.body && response.body.certificate) {
const cert = response.body.certificate;
certificate = await Certificate.create({
Expand Down
6 changes: 4 additions & 2 deletions packages/agent/src/agent/http/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ test('should adjust the Expiry if the clock is more than 30 seconds behind', asy
const agent = new HttpAgent({ host: HTTP_AGENT_HOST, fetch: mockFetch });

await agent.syncTime();
agent.overrideSystemTime = true;

await agent
.call(Principal.managementCanister(), {
Expand All @@ -629,7 +630,7 @@ test('should adjust the Expiry if the clock is more than 30 seconds behind', asy

const requestBody: any = cbor.decode(mockFetch.mock.calls[0][1].body);

expect(requestBody.content.ingress_expiry).toMatchInlineSnapshot(`1260000000000`);
expect(requestBody.content.ingress_expiry).toMatchInlineSnapshot(`1320000000000`);

jest.resetModules();
});
Expand All @@ -655,6 +656,7 @@ test('should adjust the Expiry if the clock is more than 30 seconds ahead', asyn
const agent = new HttpAgent({ host: HTTP_AGENT_HOST, fetch: mockFetch });

await agent.syncTime();
agent.overrideSystemTime = true;

await agent
.call(Principal.managementCanister(), {
Expand All @@ -666,7 +668,7 @@ test('should adjust the Expiry if the clock is more than 30 seconds ahead', asyn

const requestBody: any = cbor.decode(mockFetch.mock.calls[0][1].body);

expect(requestBody.content.ingress_expiry).toMatchInlineSnapshot(`1200000000000`);
expect(requestBody.content.ingress_expiry).toMatchInlineSnapshot(`1260000000000`);

jest.resetModules();
});
Expand Down
27 changes: 11 additions & 16 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ interface V1HttpAgentInterface {
_isAgent: true;
}

/**
/**
* A HTTP agent allows users to interact with a client of the internet computer
using the available methods. It exposes an API that closely follows the
public view of the internet computer, and is not intended to be exposed
Expand Down Expand Up @@ -264,6 +264,7 @@ export class HttpAgent implements Agent {
// Manage the time offset between the client and the replica
#initialClientTime: Date = new Date(Date.now());
#initialReplicaTime: Date = new Date(Date.now());
public overrideSystemTime: boolean = false;
get replicaTime(): Date {
const offset = Date.now() - this.#initialClientTime.getTime();
return new Date(this.#initialReplicaTime.getTime() + offset);
Expand Down Expand Up @@ -432,7 +433,7 @@ export class HttpAgent implements Agent {
identity?: Identity | Promise<Identity>,
): Promise<SubmitResponse> {
const callSync = options.callSync ?? true;
const id = await (identity !== undefined ? await identity : await this.#identity);
const id = await (identity !== undefined ? identity : this.#identity);
if (!id) {
throw new IdentityInvalidError(
"This identity has expired due this application's security policy. Please refresh your authentication.",
Expand All @@ -445,13 +446,10 @@ export class HttpAgent implements Agent {

const sender: Principal = id.getPrincipal() || Principal.anonymous();

let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);

// If the value is off by more than 30 seconds, reconcile system time with the network
const timeDiffMsecs = this.replicaTime && this.replicaTime.getTime() - Date.now();
if (Math.abs(timeDiffMsecs) > 1_000 * 30) {
ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + timeDiffMsecs);
}
const ingress_expiry = new Expiry(
DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS,
this.overrideSystemTime ? this.replicaTime.getTime() : Date.now()
);

const submit: CallRequest = {
request_type: SubmitRequestType.Call,
Expand Down Expand Up @@ -789,13 +787,10 @@ export class HttpAgent implements Agent {
const canister = Principal.from(canisterId);
const sender = id?.getPrincipal() || Principal.anonymous();

let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);

// If the value is off by more than 30 seconds, reconcile system time with the network
const timeDiffMsecs = this.replicaTime && this.replicaTime.getTime() - Date.now();
if (Math.abs(timeDiffMsecs) > 1_000 * 30) {
ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + timeDiffMsecs);
}
const ingress_expiry = new Expiry(
DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS,
this.overrideSystemTime ? this.replicaTime.getTime() : Date.now()
);

const request: QueryRequest = {
request_type: ReadRequestType.Query,
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/agent/http/transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ test('it should round down to the nearest minute', () => {
jest.setSystemTime(new Date(1619459231314));

const expiry = new Expiry(5 * 60 * 1000);
expect(expiry['_value']).toEqual(BigInt(1619459460000000000));
expect(expiry['_value']).toEqual(BigInt(1619459520000000000));

const expiry_as_date_string = new Date(
Number(expiry['_value'] / BigInt(1_000_000)),
).toISOString();
expect(expiry_as_date_string).toBe('2021-04-26T17:51:00.000Z');
expect(expiry_as_date_string).toBe('2021-04-26T17:52:00.000Z');
});
6 changes: 2 additions & 4 deletions packages/agent/src/agent/http/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ import {

const NANOSECONDS_PER_MILLISECONDS = BigInt(1_000_000);

const REPLICA_PERMITTED_DRIFT_MILLISECONDS = 60 * 1000;

export class Expiry {
private readonly _value: bigint;

constructor(deltaInMSec: number) {
constructor(deltaInMSec: number, systemTime: number = Date.now()) {
// Use bigint because it can overflow the maximum number allowed in a double float.
const raw_value =
BigInt(Math.floor(Date.now() + deltaInMSec - REPLICA_PERMITTED_DRIFT_MILLISECONDS)) *
BigInt(Math.floor(systemTime + deltaInMSec)) *
NANOSECONDS_PER_MILLISECONDS;

// round down to the nearest second
Expand Down
Loading