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: reset for counter, and tests run in parallel #789

Closed
wants to merge 6 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
1 change: 1 addition & 0 deletions docs/generated/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1>Agent-JS Changelog</h1>
<section>
<h2>Version x.x.x</h2>
<ul>
<li>feat: adds subnet metrics decoding to canisterStatus for `/subnet` path</li>
<li>
chore: replaces use of localhost with 127.0.0.1 for better node 18 support. Also swaps
Jest for vitest, runs mitm against mainnet, and updates some packages
Expand Down
49 changes: 49 additions & 0 deletions packages/agent/src/agent/http/call.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Actor } from '../../actor';
import { AnonymousIdentity } from '../../auth';
import { Call } from './call';
import { IDL } from '@dfinity/candid';
import { HttpAgent } from '.';
import { Principal } from '@dfinity/principal';

const setup = () =>
new Call({
identity: new AnonymousIdentity(),
canisterId: 'ivcos-eqaaa-aaaab-qablq-cai',
callArgs: {
arg: new ArrayBuffer(0),
methodName: 'whoami',
},
maxTries: 3,
fetchConfig: {
body: new ArrayBuffer(0),
method: 'POST',
headers: {},
fetch,
host: 'https://icp-api.io',
},
});

jest.setTimeout(30000);
test('makes a call', async () => {
jest.useRealTimers();
const call = setup();
expect(call).toBeInstanceOf(Call);
const { response, requestId } = await call.request();
expect(response).toBeTruthy();
});

test('actor', async () => {
const actor = Actor.createActor(
() => {
return IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});
},
{
agent: new HttpAgent({ host: 'https://icp-api.io' }),
canisterId: 'ivcos-eqaaa-aaaab-qablq-cai',
},
) as Actor & { whoami: () => Promise<Principal> };
const whoami = await actor.whoami();
expect(whoami.toText()).toBe('2vxsx-fae');
});
247 changes: 247 additions & 0 deletions packages/agent/src/agent/http/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { Principal } from '@dfinity/principal';
import { Identity } from '../../auth';
import { Expiry, httpHeadersTransform } from './transforms';
import {
CallRequest,
Endpoint,
HttpAgentRequest,
HttpAgentRequestTransformFn,
HttpHeaderField,
SubmitRequestType,
} from './types';

import * as cbor from '../../cbor';
import { SubmitResponse } from '../api';
import { AgentError } from '../../errors';
import { RequestId, requestIdOf } from '../../request_id';

const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000;

export type CallOptions = {
canisterId: Principal | string;
callArgs: {
methodName: string;
arg: ArrayBuffer;
effectiveCanisterId?: Principal | string;
};
maxTries: number;
identity: Identity | Promise<Identity>;
fetchConfig: FetchConfig;
callOptions?: Record<string, unknown>;
credentials?: string;
};

export type CallResponse = {
response: {
ok: boolean;
status: number;
statusText: string;
body: {
error_code?: string | undefined;
reject_code: number;
reject_message: string;
} | null;
headers: HttpHeaderField[];
};
requestId: RequestId;
};

export type FetchConfig = {
body: ArrayBuffer;
method: string;
headers: Record<string, string>;
fetch: typeof fetch;
host: string;
};

class AgentCallError extends AgentError {
constructor(message: string) {
super(message);
}
}

/**
* Call is a wrapper around a call to a canister.
* It manages the state of the call and provides
* methods for retrying the call.
*/
export class Call {
#options: CallOptions;
#tries = 0;
#maxTries = 3;
#timeDiffMsecs = 0;
#pipeline: HttpAgentRequestTransformFn[] = [];
#lastError?: AgentCallError;

constructor(options: CallOptions) {
this.#options = options;
}

get options() {
return this.#options;
}

get tries() {
return this.#tries;
}

get maxTries() {
return this.#maxTries;
}

public async request(): Promise<CallResponse> {
this.#tries; //?
while (this.#tries < this.#maxTries) {
try {
return await this.#try();
} catch (e) {
console.log(e);
}
}
throw new AgentCallError('Max tries reached');
}

async #exponentialBackoff(cb: () => Promise<CallResponse>): Promise<CallResponse> {
const delay = 2 ** this.#tries * 100;
delay;
// await new Promise(resolve => setTimeout(resolve, delay));
return await cb();
}

async #try(): Promise<CallResponse> {
if (this.#tries >= this.#maxTries) {
if (this.#lastError) {
throw this.#lastError;
}
throw new AgentCallError('Max tries reached');
}
this.#tries++;

const { canisterId, identity, callArgs, credentials, fetchConfig, callOptions } = this.#options;

const id = await identity;

const canister = Principal.from(canisterId);
const ecid = callArgs.effectiveCanisterId
? Principal.from(callArgs.effectiveCanisterId)
: canister;

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

const ingress_expiry = new Expiry(
DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS,
BigInt(this.#timeDiffMsecs),
);

const submit: CallRequest = {
request_type: SubmitRequestType.Call,
canister_id: canister,
method_name: callArgs.methodName,
arg: callArgs.arg,
sender,
ingress_expiry,
};

let transformedRequest = await this.#transform({
request: {
method: 'POST',
headers: {
'Content-Type': 'application/cbor',
...(credentials ? { Authorization: 'Basic ' + btoa(credentials) } : {}),
},
},
endpoint: Endpoint.Call,
body: submit,
});

transformedRequest = (await id.transformRequest(transformedRequest)) as HttpAgentRequest;

const body = cbor.encode(transformedRequest.body);

const { host } = fetchConfig;

return fetch('' + new URL(`/api/v2/canister/${ecid.toText()}/call`, host), {
...callOptions,
...transformedRequest.request,
body,
} as RequestInit)
.then(async response => {
if (response.status === 401) {
console.log(response.status, response.statusText);
throw new Error('Unauthorized');
}

if (response.status === 404) {
console.log(response.status, response.statusText);
throw new Error('Canister not found');
}

if (!response.ok) {
this.#lastError = new AgentCallError(
`Server returned an error:\n` +
` Code: ${response.status} (${response.statusText})\n` +
` Body: ${await response.clone().text()}\n`,
);

const responseText = await response.clone().text();

// Handle time drift errors
if (responseText.includes('Specified ingress_expiry')) {
const errorParts = responseText.split(': ');
errorParts;
const minExpiry = new Date(errorParts[2].split(',')[0].trim()); //?
const maxExpiry = new Date(errorParts[3].split(',')[0].trim()); //?
const providedExpiry = new Date(errorParts[4].trim()); //?

const result = {
minimum: minExpiry,
maximum: maxExpiry,
provided: providedExpiry,
};

console.log(
'HttpAgent has detected a disagreement about time with the replica. Retrying with adjusted expiry.',
result,
);
// Adjust the time difference to account for the time it took to make the request.
this.#timeDiffMsecs = maxExpiry.getTime() - Date.now() - 1000;
console.log('Adjusted time difference to', this.#timeDiffMsecs, 'milliseconds.');
}
}

const responseBuffer = await response.arrayBuffer();
const responseBody = (
response.status === 200 && responseBuffer.byteLength > 0
? cbor.decode(responseBuffer)
: null
) as SubmitResponse['response']['body'];

const requestId = requestIdOf(submit);

return {
response: {
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseBody,
headers: httpHeadersTransform(response.headers),
},
requestId,
};
})
.catch(e => {
console.log(e);
throw e;
});
}

#transform = async (request: HttpAgentRequest): Promise<HttpAgentRequest> => {
let p = Promise.resolve(request);

for (const fn of this.#pipeline) {
p = p.then(r => fn(r).then(r2 => r2 || r));
}

return p;
};
}
9 changes: 9 additions & 0 deletions packages/agent/src/certificate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,15 @@ describe('node keys', () => {
const nodeKeys = cert.cache_node_keys();
expect(nodeKeys).toMatchInlineSnapshot(`
Object {
"metrics": Object {
"canister_state_bytes": 10007399447n,
"consumed_cycles_total": Object {
"current": 15136490391288n,
"deleted": 0n,
},
"num_canisters": 451n,
"update_transactions_total": 222360n,
},
"nodeKeys": Array [
"302a300506032b65700321005b0bdf0329932ab0a78fa7192ad76cf37d67eb2024739774d3b67da7799ebc9c",
"302a300506032b65700321009776d25542873dafb8099303d1fca8e4aa344e73cf5a3d7df5b40f9c8ed5a085",
Expand Down
Loading
Loading