From 90a3e1f6b29ec92e6f80f295a4521aafd0faee67 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Fri, 21 Jun 2024 17:08:16 +0200 Subject: [PATCH 1/2] feat(resource-rental): creation of exe-unit during resource-rental startup --- src/resource-rental/resource-rental.test.ts | 68 +++++++++++++++++++++ src/resource-rental/resource-rental.ts | 46 +++++++++----- 2 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/resource-rental/resource-rental.test.ts diff --git a/src/resource-rental/resource-rental.test.ts b/src/resource-rental/resource-rental.test.ts new file mode 100644 index 000000000..8473e85ea --- /dev/null +++ b/src/resource-rental/resource-rental.test.ts @@ -0,0 +1,68 @@ +import { imock, instance, mock, reset, when, verify, _ } from "@johanblumenberg/ts-mockito"; +import { Agreement, MarketModule } from "../market"; +import { StorageProvider } from "../shared/storage"; +import { AgreementPaymentProcess } from "../payment/agreement_payment_process"; +import { ResourceRental, ResourceRentalOptions } from "."; +import { ActivityModule, ExeUnit } from "../activity"; +import { Logger } from "../shared/utils"; + +const mockAgreement = mock(Agreement); +const mockStorageProvider = imock(); +const mockPaymentProcess = mock(AgreementPaymentProcess); +const mockMarketModule = imock(); +const mockActivityModule = imock(); +const mockLogger = imock(); +const mockResourceRentalOptions = imock(); + +let resourceRental: ResourceRental; + +beforeEach(() => { + reset(mockAgreement); + reset(mockStorageProvider); + reset(mockPaymentProcess); + reset(mockMarketModule); + reset(mockActivityModule); + reset(mockLogger); + reset(mockResourceRentalOptions); + when(mockActivityModule.createExeUnit(_, _)).thenResolve(instance(mock(ExeUnit))); + resourceRental = new ResourceRental( + instance(mockAgreement), + instance(mockStorageProvider), + instance(mockPaymentProcess), + instance(mockMarketModule), + instance(mockActivityModule), + instance(mockLogger), + instance(mockResourceRentalOptions), + ); +}); + +describe("ResourceRental", () => { + describe("ExeUnit", () => { + it("should create an exe unit on startup and use it later", async () => { + expect(resourceRental["currentExeUnit"]).toBeDefined(); + verify(mockActivityModule.createExeUnit(_, _)).once(); + await resourceRental.getExeUnit(); + verify(mockActivityModule.createExeUnit(_, _)).once(); + }); + + it("should reuse the same promise if called multiple times", async () => { + expect(resourceRental["currentExeUnit"]).toBeDefined(); + const promise1 = resourceRental.getExeUnit(); + const promise2 = resourceRental.getExeUnit(); + const promise3 = resourceRental.getExeUnit(); + await Promise.all([promise1, promise2, promise3]); + verify(mockActivityModule.createExeUnit(_, _)).once(); + }); + + it("should reuse the same promise if called multiple time after destroy exe-unit created on strtup", async () => { + expect(resourceRental["currentExeUnit"]).toBeDefined(); + await resourceRental.destroyExeUnit(); + const promise1 = resourceRental.getExeUnit(); + const promise2 = resourceRental.getExeUnit(); + const promise3 = resourceRental.getExeUnit(); + expect(resourceRental["exeUnitPromise"]).toBeDefined(); + await Promise.all([promise1, promise2, promise3]); + verify(mockActivityModule.createExeUnit(_, _)).twice(); + }); + }); +}); diff --git a/src/resource-rental/resource-rental.ts b/src/resource-rental/resource-rental.ts index 3f80c1901..6f63a3862 100644 --- a/src/resource-rental/resource-rental.ts +++ b/src/resource-rental/resource-rental.ts @@ -34,6 +34,7 @@ export class ResourceRental { private currentExeUnit: ExeUnit | null = null; private abortController = new AbortController(); private finalizePromise?: Promise; + private exeUnitPromise?: Promise; public constructor( public readonly agreement: Agreement, @@ -45,7 +46,7 @@ export class ResourceRental { private readonly resourceRentalOptions?: ResourceRentalOptions, ) { this.networkNode = this.resourceRentalOptions?.networkNode; - + this.createExeUnit(this.abortController.signal); // TODO: Listen to agreement events to know when it goes down due to provider closing it! } @@ -114,10 +115,9 @@ export class ResourceRental { if (this.finalizePromise || this.abortController.signal.aborted) { throw new GolemUserError("The resource rental is not active. It may have been aborted or finalized"); } - if (this.currentExeUnit) { + if (this.currentExeUnit !== null) { return this.currentExeUnit; } - const abortController = new AbortController(); this.abortController.signal.addEventListener("abort", () => abortController.abort(this.abortController.signal.reason), @@ -129,29 +129,43 @@ export class ResourceRental { abortController.abort(signalOrTimeout.reason); } } - - const activity = await this.activityModule.createActivity(this.agreement); - this.currentExeUnit = await this.activityModule.createExeUnit(activity, { - storageProvider: this.storageProvider, - networkNode: this.resourceRentalOptions?.networkNode, - executionOptions: this.resourceRentalOptions?.activity, - signalOrTimeout: abortController.signal, - ...this.resourceRentalOptions?.exeUnit, - }); - - return this.currentExeUnit; + return this.createExeUnit(abortController.signal); } + /** + * Destroy previously created exe-unit. + * Please note that if ResourceRental is left without ExeUnit for some time (default 90s) + * the provider will terminate the Agreement and ResourceRental will be unuseble + */ async destroyExeUnit() { - if (this.currentExeUnit) { + if (this.currentExeUnit !== null) { await this.activityModule.destroyActivity(this.currentExeUnit.activity); this.currentExeUnit = null; } else { - throw new Error(`There is no activity to destroy.`); + throw new GolemUserError(`There is no exe-unit to destroy.`); } } async fetchAgreementState() { return this.marketModule.fetchAgreement(this.agreement.id).then((agreement) => agreement.getState()); } + + private async createExeUnit(abortSignal: AbortSignal) { + if (!this.exeUnitPromise) { + this.exeUnitPromise = (async () => { + const activity = await this.activityModule.createActivity(this.agreement); + this.currentExeUnit = await this.activityModule.createExeUnit(activity, { + storageProvider: this.storageProvider, + networkNode: this.resourceRentalOptions?.networkNode, + executionOptions: this.resourceRentalOptions?.activity, + signalOrTimeout: abortSignal, + ...this.resourceRentalOptions?.exeUnit, + }); + return this.currentExeUnit; + })().finally(() => { + this.exeUnitPromise = undefined; + }); + } + return this.exeUnitPromise; + } } From 923ea8398a64eddfacea29699b739fa4d56c8e1c Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Thu, 27 Jun 2024 10:40:18 +0200 Subject: [PATCH 2/2] chore: improved error handling --- src/resource-rental/resource-rental.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/resource-rental/resource-rental.ts b/src/resource-rental/resource-rental.ts index 6f63a3862..54d11b6b6 100644 --- a/src/resource-rental/resource-rental.ts +++ b/src/resource-rental/resource-rental.ts @@ -162,9 +162,14 @@ export class ResourceRental { ...this.resourceRentalOptions?.exeUnit, }); return this.currentExeUnit; - })().finally(() => { - this.exeUnitPromise = undefined; - }); + })() + .catch((error) => { + this.logger.error(`Failed to create exe-unit. ${error}`, { agreementId: this.agreement.id }); + throw error; + }) + .finally(() => { + this.exeUnitPromise = undefined; + }); } return this.exeUnitPromise; }