diff --git a/daprdocs/content/en/js-sdk-docs/js-actors/_index.md b/daprdocs/content/en/js-sdk-docs/js-actors/_index.md index 16740714..08276cd2 100644 --- a/daprdocs/content/en/js-sdk-docs/js-actors/_index.md +++ b/daprdocs/content/en/js-sdk-docs/js-actors/_index.md @@ -158,7 +158,8 @@ async function start() await client.actor.timerCreate(ParkingSensorImpl.name, `actor-id`, `timer-id`, { callback: "method-to-excute-on-actor", dueTime: Temporal.Duration.from({ seconds: 2 }), - period: Temporal.Duration.from({ seconds: 1 }) + period: Temporal.Duration.from({ seconds: 1 }), + ttl: Temporal.Duration.from({ seconds: 1 }), }); // Delete the timer @@ -184,6 +185,7 @@ async function start() await client.actor.reminderCreate(DemoActorImpl.name, `actor-id`, `timer-id`, { dueTime: Temporal.Duration.from({ seconds: 2 }), period: Temporal.Duration.from({ seconds: 1 }), + ttl: Temporal.Duration.from({ seconds: 1 }), data: 100 }); diff --git a/src/actors/client/ActorClient/ActorClientGRPC.ts b/src/actors/client/ActorClient/ActorClientGRPC.ts index 71db5d72..d62437e6 100644 --- a/src/actors/client/ActorClient/ActorClientGRPC.ts +++ b/src/actors/client/ActorClient/ActorClientGRPC.ts @@ -137,6 +137,10 @@ export default class ActorClientGRPC implements IClientActor { msgService.setDueTime(reminder.dueTime.toString()); } + if (reminder.ttl) { + msgService.setTtl(reminder.ttl.toString()); + } + return new Promise((resolve, reject) => { const client = this.client.getClient(); client.registerActorReminder(msgService, (err, _res) => { @@ -191,6 +195,10 @@ export default class ActorClientGRPC implements IClientActor { msgService.setDueTime(timer.dueTime.toString()); } + if (timer.ttl) { + msgService.setTtl(timer.ttl.toString()); + } + return new Promise((resolve, reject) => { const client = this.client.getClient(); client.registerActorTimer(msgService, (err, _res) => { diff --git a/src/actors/client/ActorClient/ActorClientHTTP.ts b/src/actors/client/ActorClient/ActorClientHTTP.ts index 364b83f6..8015a000 100644 --- a/src/actors/client/ActorClient/ActorClientHTTP.ts +++ b/src/actors/client/ActorClient/ActorClientHTTP.ts @@ -60,6 +60,7 @@ export default class ActorClientHTTP implements IClientActor { body: JSON.stringify({ period: reminder.period.toString().toLocaleLowerCase().replace('pt', ''), dueTime: reminder?.dueTime?.toString()?.toLocaleLowerCase().replace('pt', ''), + ttl: reminder?.ttl?.toString()?.toLocaleLowerCase().replace('pt', ''), data: reminder.data }), }); @@ -85,6 +86,7 @@ export default class ActorClientHTTP implements IClientActor { body: JSON.stringify({ period: timer.period.toString().toLocaleLowerCase().replace('pt', ''), dueTime: timer?.dueTime?.toString()?.toLocaleLowerCase().replace('pt', ''), + ttl: timer?.ttl?.toString()?.toLocaleLowerCase().replace('pt', ''), data: timer.data, callback: timer.callback }), diff --git a/src/actors/runtime/AbstractActor.ts b/src/actors/runtime/AbstractActor.ts index 8777c748..bcafc349 100644 --- a/src/actors/runtime/AbstractActor.ts +++ b/src/actors/runtime/AbstractActor.ts @@ -80,14 +80,16 @@ export default abstract class AbstractActor { * @param reminderName name of the reminder * @param state the state to be send along with the reminder trigger * @param dueTime due time for the first trigger + * @param ttl time to duration after which the reminder will be expired and deleted * @param period frequency for the triggers * @param Type of the state object * @return Async void response */ - async registerActorReminder<_Type>(reminderName: string, dueTime: Temporal.Duration, period: Temporal.Duration, state?: any) { + async registerActorReminder<_Type>(reminderName: string, dueTime: Temporal.Duration, period: Temporal.Duration, ttl?: Temporal.Duration, state?: any) { await this.actorClient.actor.registerActorReminder(this.actorType, this.id, reminderName, { period, dueTime, + ttl, data: state }); } @@ -96,11 +98,12 @@ export default abstract class AbstractActor { await this.actorClient.actor.unregisterActorReminder(this.actorType, this.id, reminderName); } - async registerActorTimer(timerName: string, callback: string, dueTime: Temporal.Duration, period: Temporal.Duration, state?: any) { + async registerActorTimer(timerName: string, callback: string, dueTime: Temporal.Duration, period: Temporal.Duration, ttl?: Temporal.Duration, state?: any) { // Register the timer in the sidecar return await this.actorClient.actor.registerActorTimer(this.actorType, this.id, timerName, { period, dueTime, + ttl, data: state, callback }); diff --git a/src/actors/runtime/ActorReminderData.ts b/src/actors/runtime/ActorReminderData.ts index 34c773be..ed8e8878 100644 --- a/src/actors/runtime/ActorReminderData.ts +++ b/src/actors/runtime/ActorReminderData.ts @@ -20,17 +20,20 @@ export default class ActorReminderData { readonly reminderName: string; readonly state: string | object | undefined; readonly dueTime: number; + readonly ttl: number | undefined; readonly period: number; /** * @param reminderName the name of the actor reminder * @param state the state data passed to receiveReminder callback * @param dueTime the amount of time to delay before invoking the reminder for the first time + * @param ttl time to duration after which the reminder will be expired and deleted * @param period the time interval between reminder invocations after the first invocation */ - constructor(reminderName: string, dueTime: number, period: number, state?: string | object) { + constructor(reminderName: string, dueTime: number, period: number, ttl?: number, state?: string | object) { this.reminderName = reminderName; this.dueTime = dueTime; + this.ttl = ttl; this.period = period; this.state = state; } @@ -47,6 +50,10 @@ export default class ActorReminderData { return this.dueTime; } + getTtl(): number | undefined { + return this.ttl; + } + getPeriod(): number { return this.period; } @@ -58,6 +65,7 @@ export default class ActorReminderData { return { reminderName: this.reminderName, dueTime: this.dueTime, + ttl: this.ttl, period: this.period, data: this.state } @@ -68,10 +76,11 @@ export default class ActorReminderData { const data = obj?.data; const dueTime = obj?.dueTime; + const ttl = obj?.ttl; const period = obj?.period; const deserializedData = serializer.deserialize(data); - return new ActorReminderData(reminderName, dueTime, period, deserializedData); + return new ActorReminderData(reminderName, dueTime, ttl, period, deserializedData); } } \ No newline at end of file diff --git a/src/types/ActorReminder.type.ts b/src/types/ActorReminder.type.ts index 89870b1a..0827f06f 100644 --- a/src/types/ActorReminder.type.ts +++ b/src/types/ActorReminder.type.ts @@ -17,4 +17,5 @@ export type ActorReminderType = { period: Temporal.Duration; // e.g. 0h0m9s0ms dueTime?: Temporal.Duration; // e.g. 1m or 0h0m0s0ms defaults to 0s data?: any; // the data to pass + ttl?: Temporal.Duration; // e.g. 1m } \ No newline at end of file diff --git a/src/types/ActorTimer.type.ts b/src/types/ActorTimer.type.ts index 3d36e1ff..2e615731 100644 --- a/src/types/ActorTimer.type.ts +++ b/src/types/ActorTimer.type.ts @@ -17,5 +17,6 @@ export type ActorTimerType = { period: Temporal.Duration; // e.g. 0h0m9s0ms dueTime?: Temporal.Duration; // e.g. 1m or 0h0m0s0ms defaults to 0s data?: any; // the data to pass + ttl?: Temporal.Duration; // e.g. 1m callback: string; // which method to execute as callback method } \ No newline at end of file diff --git a/test/actor/DemoActorReminder2Impl.ts b/test/actor/DemoActorReminder2Impl.ts index 700cd0b6..9bb14abc 100644 --- a/test/actor/DemoActorReminder2Impl.ts +++ b/test/actor/DemoActorReminder2Impl.ts @@ -18,7 +18,8 @@ export default class DemoActorReminder2Impl extends AbstractActor implements Dem counter = 0; async init(): Promise { - await super.registerActorReminder("my-reminder-name", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 1 }), 123); + await super.registerActorReminder("my-reminder-name", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 2 }), + Temporal.Duration.from({ seconds: 1 }), 123); return "Actor Initialized"; } diff --git a/test/actor/DemoActorReminderImpl.ts b/test/actor/DemoActorReminderImpl.ts index e6b104ed..0d8d28f4 100644 --- a/test/actor/DemoActorReminderImpl.ts +++ b/test/actor/DemoActorReminderImpl.ts @@ -18,7 +18,8 @@ export default class DemoActorReminderImpl extends AbstractActor implements Demo counter = 0; async init(): Promise { - await super.registerActorReminder("my-reminder-name", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 1 }), 123); + await super.registerActorReminder("my-reminder-name", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 3 }), + undefined, 123); return "Actor Initialized"; } diff --git a/test/actor/DemoActorReminderTtlImpl.ts b/test/actor/DemoActorReminderTtlImpl.ts new file mode 100644 index 00000000..e47b28a3 --- /dev/null +++ b/test/actor/DemoActorReminderTtlImpl.ts @@ -0,0 +1,48 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { AbstractActor, Temporal } from "../../src"; +import DemoActorReminderInterface from "./DemoActorReminderInterface"; + +export default class DemoActorReminderTtlImpl extends AbstractActor implements DemoActorReminderInterface { + counter = 0; + + async init(): Promise { + await super.registerActorReminder("my-reminder-name", + Temporal.Duration.from({ seconds: 2 }), //dueTime + Temporal.Duration.from({ seconds: 2 }), //period + Temporal.Duration.from({ seconds: 1 }), //ttl + 123); + return "Actor Initialized"; + } + + async count(): Promise { + this.counter++; + } + + async getCounter(): Promise { + return this.counter; + } + + async removeReminder(): Promise { + return this.unregisterActorReminder("my-reminder-name"); + } + + /** + * @override + * @param data + */ + async receiveReminder(data: string): Promise { + this.counter += parseInt(data); + } +} \ No newline at end of file diff --git a/test/actor/DemoActorTimerImpl.ts b/test/actor/DemoActorTimerImpl.ts index 7d92fbbf..502b9db1 100644 --- a/test/actor/DemoActorTimerImpl.ts +++ b/test/actor/DemoActorTimerImpl.ts @@ -18,7 +18,8 @@ export default class DemoActorTimerImpl extends AbstractActor implements DemoAct counter = 0; async init(): Promise { - await super.registerActorTimer("my-timer-name", "countBy", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 1 }), 100); + await super.registerActorTimer("my-timer-name", "countBy", Temporal.Duration.from({ seconds: 2 }), Temporal.Duration.from({ seconds: 1 }), + undefined, 100); return "Actor Initialized"; } diff --git a/test/actor/DemoActorTimerTtlImpl.ts b/test/actor/DemoActorTimerTtlImpl.ts new file mode 100644 index 00000000..2889ae3d --- /dev/null +++ b/test/actor/DemoActorTimerTtlImpl.ts @@ -0,0 +1,44 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { AbstractActor, Temporal } from "../../src"; +import DemoActorTimerInterface from "./DemoActorTimerInterface"; + +export default class DemoActorTimerTtlImpl extends AbstractActor implements DemoActorTimerInterface { + counter = 0; + + async init(): Promise { + await super.registerActorTimer("my-timer-name", "countBy", + Temporal.Duration.from({ seconds: 2 }), //dueTime + Temporal.Duration.from({ seconds: 2 }), //period + Temporal.Duration.from({ seconds: 3 }), //ttl + 100); + return "Actor Initialized"; + } + + async count(): Promise { + this.counter++; + } + + async getCounter(): Promise { + return this.counter; + } + + async countBy(amount: string): Promise { + this.counter += parseInt(amount); + } + + async removeTimer(): Promise { + return await this.unregisterActorTimer("my-timer-name"); + } +} \ No newline at end of file diff --git a/test/e2e/actors.http.test.ts b/test/e2e/actors.http.test.ts index 79811d3c..e2c35265 100644 --- a/test/e2e/actors.http.test.ts +++ b/test/e2e/actors.http.test.ts @@ -26,6 +26,8 @@ import DemoActorSayImpl from '../actor/DemoActorSayImpl'; import DemoActorSayInterface from '../actor/DemoActorSayInterface'; import DemoActorTimerImpl from '../actor/DemoActorTimerImpl'; import DemoActorTimerInterface from '../actor/DemoActorTimerInterface'; +import DemoActorTimerTtlImpl from '../actor/DemoActorTimerTtlImpl'; +import DemoActorReminderTtlImpl from '../actor/DemoActorReminderTtlImpl'; const serverHost = "127.0.0.1"; const serverPort = "50001"; @@ -61,6 +63,8 @@ describe('http/actors', () => { await server.actor.registerActor(DemoActorReminder2Impl); await server.actor.registerActor(DemoActorTimerImpl); await server.actor.registerActor(DemoActorActivateImpl); + await server.actor.registerActor(DemoActorTimerTtlImpl); + await server.actor.registerActor(DemoActorReminderTtlImpl); // Start server await server.start(); // Start the general server, this can take a while @@ -97,13 +101,15 @@ describe('http/actors', () => { it('should register actors correctly', async () => { const actors = await server.actor.getRegisteredActors(); - expect(actors.length).toEqual(6); + expect(actors.length).toEqual(8); expect(actors).toContain(DemoActorCounterImpl.name); expect(actors).toContain(DemoActorSayImpl.name); expect(actors).toContain(DemoActorReminderImpl.name); expect(actors).toContain(DemoActorTimerImpl.name); expect(actors).toContain(DemoActorActivateImpl.name); + expect(actors).toContain(DemoActorTimerTtlImpl.name); + expect(actors).toContain(DemoActorReminderTtlImpl.name); }); it('should be able to invoke an actor through a text message', async () => { @@ -171,6 +177,45 @@ describe('http/actors', () => { const res4 = await actor.getCounter(); expect(res4).toEqual(300); }, 10000); + + + it('should apply the ttl when it is set (expected execution time > 5s)', async () => { + const builder = new ActorProxyBuilder(DemoActorTimerTtlImpl, client); + const actor = builder.build(ActorId.createRandomId()); + + // Activate our actor + await actor.init(); + + const res0 = await actor.getCounter(); + expect(res0).toEqual(0); + + // Now we wait for dueTime (2s) + await (new Promise(resolve => setTimeout(resolve, 2000))); + + // After that the timer callback will be called + // In our case, the callback increments the count attribute + // the count attribute is +100 due to the passed state + const res1 = await actor.getCounter(); + expect(res1).toEqual(100); + + // Every 1 second the timer gets called again, so the count attribute should change + // we check this twice to ensure correct calling + await (new Promise(resolve => setTimeout(resolve, 1000))); + const res2 = await actor.getCounter(); + expect(res2).toEqual(100); + await (new Promise(resolve => setTimeout(resolve, 1000))); + const res3 = await actor.getCounter(); + expect(res3).toEqual(200); + + // Stop the timer + await actor.removeTimer(); + + // We then expect the counter to stop increasing + await (new Promise(resolve => setTimeout(resolve, 1000))); + const res4 = await actor.getCounter(); + expect(res4).toEqual(200); + + }, 10000); }); describe('reminders', () => { @@ -228,5 +273,33 @@ describe('http/actors', () => { // Unregister the reminder await actor.removeReminder(); }); + + it('should apply the ttl when it is set to a reminder', async () => { + const builder = new ActorProxyBuilder(DemoActorReminderTtlImpl, client); + const actor = builder.build(ActorId.createRandomId()); + + // Activate our actor + // this will initialize the reminder to be called + await actor.init(); + + const res0 = await actor.getCounter(); + expect(res0).toEqual(0); + + // Now we wait for dueTime (2s) + await NodeJSUtil.sleep(2000); + + // After that the reminder callback will be called + // In our case, the callback increments the count attribute + const res1 = await actor.getCounter(); + expect(res1).toEqual(123); + + await actor.removeReminder(); + + await (new Promise(resolve => setTimeout(resolve, 2000))); + + // Make sure the counter didn't change + const res2 = await actor.getCounter(); + expect(res2).toEqual(123); + }); }); }); \ No newline at end of file