Skip to content

Commit

Permalink
feat(core): long running operation recovery (#882)
Browse files Browse the repository at this point in the history
* feat: basic record to mark restore as complete

* feat: mark done when finished recovering in the DB

* feat: update unittest

* update: resolve comment

* update: update unit test

* feat: resolve comment

* feat: long running operation recovery

* remove duplicate logic

* feat(core): recovery of individual IPEX on-going messages and general recovery method

* refactor: remove comment (ticket created)

* test(core): recovery of ipex exns

---------

Co-authored-by: iFergal <[email protected]>
  • Loading branch information
Sotatek-TungNguyen2a and iFergal authored Jan 3, 2025
1 parent 5ac7c4f commit ecb326c
Show file tree
Hide file tree
Showing 19 changed files with 488 additions and 354 deletions.
16 changes: 7 additions & 9 deletions src/core/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ const mockCredentialService = {
syncKeriaCredentials: jest.fn(),
removeCredentialsPendingDeletion: jest.fn(),
};
const mockKeriaNotificationService = {
syncIPEXReplyOperations: jest.fn(),
};

const mockEntropy = "00000000000000000000000000000000";

describe("test cases of bootAndConnect function", () => {
describe("KERIA connectivity", () => {
let agent: Agent;
let mockAgentUrls: AgentUrls;
let mockSignifyClient: any;
Expand Down Expand Up @@ -238,7 +241,7 @@ describe("test cases of bootAndConnect function", () => {
});
});

describe("test cases of recoverKeriaAgent function", () => {
describe("Recovery of DB from cloud sync", () => {
let agent: Agent;
let mockSeedPhrase: string[];
let mockConnectUrl: string;
Expand All @@ -254,6 +257,7 @@ describe("test cases of recoverKeriaAgent function", () => {
(agent as any).basicStorageService = mockBasicStorageService;
(agent as any).agentServicesProps = mockAgentServicesProps;
(agent as any).connectionService = mockConnectionService;
(agent as any).keriaNotificationService = mockKeriaNotificationService;

mockSeedPhrase = [
"abandon",
Expand Down Expand Up @@ -306,6 +310,7 @@ describe("test cases of recoverKeriaAgent function", () => {
expect(mockConnectionService.syncKeriaContacts).toHaveBeenCalled();
expect(mockIdentifierService.syncKeriaIdentifiers).toHaveBeenCalled();
expect(mockCredentialService.syncKeriaCredentials).toHaveBeenCalled();
expect(mockKeriaNotificationService.syncIPEXReplyOperations).toHaveBeenCalled();
expect(mockSignifyClient.connect).toHaveBeenCalled();
expect(mockBasicStorageService.createOrUpdateBasicRecord).toHaveBeenCalledWith({
_tags: {},
Expand All @@ -319,13 +324,6 @@ describe("test cases of recoverKeriaAgent function", () => {
KeyStoreKeys.SIGNIFY_BRAN,
expectedBran
);
expect(Agent.isOnline).toBe(true);
expect(mockAgentServicesProps.eventEmitter.emit).toBeCalledWith({
type: EventTypes.KeriaStatusChanged,
payload: {
isOnline: true,
},
});
});

test("should throw an error for invalid mnemonic", async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/core/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,14 @@ class Agent {
await this.connections.syncKeriaContacts();
await this.identifiers.syncKeriaIdentifiers();
await this.credentials.syncKeriaCredentials();
await this.keriaNotifications.syncIPEXReplyOperations();

await this.basicStorage.createOrUpdateBasicRecord(
new BasicRecord({
id: MiscRecordId.CLOUD_RECOVERY_STATUS,
content: { syncing: false },
})
);

this.markAgentStatus(true);
}

private async connectSignifyClient(): Promise<void> {
Expand All @@ -349,6 +348,7 @@ class Agent {
});
}

// For now this is called by UI/AppWrapper to not prematurely mark online while mid-recovery
markAgentStatus(online: boolean) {
Agent.isOnline = online;

Expand Down
2 changes: 1 addition & 1 deletion src/core/agent/event.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ enum EventTypes {
interface NotificationAddedEvent extends BaseEventEmitter {
type: typeof EventTypes.NotificationAdded;
payload: {
keriaNotif: KeriaNotification;
note: KeriaNotification;
};
}

Expand Down
13 changes: 8 additions & 5 deletions src/core/agent/records/notificationRecord.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseRecord, Tags } from "../../storage/storage.types";
import { NotificationRoute } from "../agent.types";
import { LinkedGroupRequest } from "./notificationRecord.types";
import { LinkedRequest } from "./notificationRecord.types";
import { randomSalt } from "../services/utils";

interface NotificationRecordStorageProps {
Expand All @@ -13,7 +13,7 @@ interface NotificationRecordStorageProps {
multisigId?: string;
connectionId: string;
credentialId?: string;
linkedGroupRequest?: LinkedGroupRequest;
linkedRequest?: LinkedRequest;
groupReplied?: boolean,
initiatorAid?: string,
groupInitiator?: boolean,
Expand All @@ -25,11 +25,12 @@ class NotificationRecord extends BaseRecord {
read!: boolean;
multisigId?: string;
connectionId!: string;
linkedGroupRequest!: LinkedGroupRequest;
linkedRequest!: LinkedRequest;
credentialId?: string;
groupReplied?: boolean;
initiatorAid?: string;
groupInitiator?: boolean;
hidden = false; // Hide from UI but don't delete (used for reliability while recovering IPEX long running operations)

static readonly type = "NotificationRecord";
readonly type = NotificationRecord.type;
Expand All @@ -45,7 +46,7 @@ class NotificationRecord extends BaseRecord {
this.multisigId = props.multisigId;
this.connectionId = props.connectionId;
this._tags = props.tags ?? {};
this.linkedGroupRequest = props.linkedGroupRequest ?? { accepted: false };
this.linkedRequest = props.linkedRequest ?? { accepted: false };
this.credentialId = props.credentialId;
this.groupReplied = props.groupReplied;
this.initiatorAid = props.initiatorAid;
Expand All @@ -55,6 +56,7 @@ class NotificationRecord extends BaseRecord {

getTags() {
return {
...this._tags,
route: this.route,
read: this.read,
multisigId: this.multisigId,
Expand All @@ -63,7 +65,8 @@ class NotificationRecord extends BaseRecord {
groupReplied: this.groupReplied,
initiatorAid: this.initiatorAid,
groupInitiator: this.groupInitiator,
...this._tags,
currentLinkedRequest: this.linkedRequest.current,
hidden: this.hidden,
};
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/core/agent/records/notificationRecord.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Notification } from "../services/credentialService.types";

interface LinkedGroupRequest {
interface LinkedRequest {
accepted: boolean;
current?: string;
previous?: string;
Expand All @@ -12,4 +12,4 @@ interface NotificationAttempts {
notification: Notification;
}

export type { LinkedGroupRequest, NotificationAttempts };
export type { LinkedRequest, NotificationAttempts };
24 changes: 9 additions & 15 deletions src/core/agent/services/identifierService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,18 +502,9 @@ class IdentifierService extends AgentService {

const op: Operation = await this.props.signifyClient
.operations()
.get(`witness.${identifier.prefix}`)
.catch(async (error) => {
const status = error.message.split(" - ")[1];
if (/404/gi.test(status)) {
return await this.props.signifyClient
.operations()
.get(`done.${identifier.prefix}`);
}
throw error;
});
.get(`witness.${identifier.prefix}`);

const isPending = !op.done;

if (isPending) {
const pendingOperation = await this.operationPendingStorage.save({
id: op.name,
Expand All @@ -531,6 +522,7 @@ class IdentifierService extends AgentService {
const identifierDetail = (await this.props.signifyClient
.identifiers()
.get(identifier.prefix)) as HabState & { icp_dt: string };

if (isMultiSig) {
const groupId = identifier.name.split(":")[1];
const groupInitiator = groupId.split("-")[0] === "1";
Expand Down Expand Up @@ -563,19 +555,21 @@ class IdentifierService extends AgentService {
if (identifier.name.startsWith("XX")) {
continue;
}

const identifierDetail = (await this.props.signifyClient
.identifiers()
.get(identifier.prefix)) as HabState & { icp_dt: string };

const multisigManageAid = identifier.group.mhab.prefix;
const groupId = identifier.group.mhab.name.split(":")[1];
const theme = parseInt(identifier.name.split(":")[0], 10);
const groupInitiator = groupId.split("-")[0] === "1";

const op = await this.props.signifyClient
.operations()
.get(`group.${identifier.prefix}`);

const isPending = !op.done;
const identifierDetail = (await this.props.signifyClient
.identifiers()
.get(identifier.prefix)) as HabState & { icp_dt: string };

if (isPending) {
const pendingOperation = await this.operationPendingStorage.save({
id: op.name,
Expand Down
Loading

0 comments on commit ecb326c

Please sign in to comment.