@pikku/schedule 0.12.4 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +27 -2
- package/dist/in-memory-scheduler-service.js +19 -2
- package/package.json +3 -2
- package/src/in-memory-scheduler-service.test.ts +61 -1
- package/src/in-memory-scheduler-service.ts +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 0.12.5
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- dbff6ae: **A delayed RPC now actually runs.** `InMemorySchedulerService.scheduleRPC`
|
|
6
|
+
dispatched through `runScheduledTask`, which resolves names against the wired
|
|
7
|
+
_cron task_ registry — a registry no internally scheduled RPC is ever in. Every
|
|
8
|
+
such call died as `ScheduledTaskNotFoundError` and was swallowed into a log
|
|
9
|
+
line, and the payload captured alongside it was never forwarded. The one that
|
|
10
|
+
mattered was `pikkuWorkflowSleeper`: it is what wakes a run from
|
|
11
|
+
`workflow.sleep`, so an async workflow containing a sleep stopped at the first
|
|
12
|
+
one and stayed `running` forever. It now invokes the RPC with its data, matching
|
|
13
|
+
what the BullMQ and pg-boss schedulers already do.
|
|
14
|
+
- Updated dependencies [41c1a95]
|
|
15
|
+
- Updated dependencies [ce96383]
|
|
16
|
+
- Updated dependencies [7e60867]
|
|
17
|
+
- Updated dependencies [f8f1244]
|
|
18
|
+
- Updated dependencies [dcf20cb]
|
|
19
|
+
- Updated dependencies [6512384]
|
|
20
|
+
- Updated dependencies [e3b4c14]
|
|
21
|
+
- Updated dependencies [efd0ed1]
|
|
22
|
+
- Updated dependencies [cba98fb]
|
|
23
|
+
- Updated dependencies [ce96383]
|
|
24
|
+
- Updated dependencies [f8f1244]
|
|
25
|
+
- Updated dependencies [f8f1244]
|
|
26
|
+
- Updated dependencies [6e93a35]
|
|
27
|
+
- Updated dependencies [6dada45]
|
|
28
|
+
- @pikku/core@0.12.80
|
|
29
|
+
|
|
1
30
|
## 0.12.4
|
|
2
31
|
|
|
3
32
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
# @pikku/
|
|
1
|
+
# @pikku/schedule
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
In-memory scheduler service for Pikku cron wirings, built on `cron`.
|
|
4
|
+
|
|
5
|
+
Runs jobs in the current process, so it suits single-instance deployments and
|
|
6
|
+
local development. For multi-instance setups use a queue-backed scheduler such
|
|
7
|
+
as `@pikku/queue-bullmq` or `@pikku/queue-pg-boss`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @pikku/schedule
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { InMemorySchedulerService } from '@pikku/schedule'
|
|
19
|
+
|
|
20
|
+
const scheduler = new InMemorySchedulerService(logger)
|
|
21
|
+
|
|
22
|
+
await scheduler.init()
|
|
23
|
+
scheduler.startAll()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Docs
|
|
27
|
+
|
|
28
|
+
https://pikku.dev/docs
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CronJob } from 'cron';
|
|
2
2
|
import { SchedulerService, parseDurationString } from '@pikku/core';
|
|
3
|
-
import { pikkuState } from '@pikku/core/
|
|
3
|
+
import { pikkuState, getSingletonServices } from '@pikku/core/ecosystem';
|
|
4
4
|
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
|
|
5
|
+
import { rpcService } from '@pikku/core/rpc';
|
|
5
6
|
const getLogger = () => {
|
|
6
7
|
return pikkuState(null, 'package', 'singletonServices').logger;
|
|
7
8
|
};
|
|
@@ -24,7 +25,23 @@ export class InMemorySchedulerService extends SchedulerService {
|
|
|
24
25
|
const timer = setTimeout(async () => {
|
|
25
26
|
this.delayedTasks.delete(taskId);
|
|
26
27
|
try {
|
|
27
|
-
|
|
28
|
+
// An RPC, not a cron task. `runScheduledTask` looks the name up in the
|
|
29
|
+
// scheduler's task registry, which only ever holds wired cron tasks —
|
|
30
|
+
// so every internally-scheduled RPC died here as
|
|
31
|
+
// ScheduledTaskNotFoundError. The one that matters most is
|
|
32
|
+
// `pikkuWorkflowSleeper`: it is registered by the workflow service as a
|
|
33
|
+
// function, and it is what wakes a run from `workflow.sleep`. With the
|
|
34
|
+
// wake-up lost to a swallowed log line, an async workflow run stopped
|
|
35
|
+
// at its first sleep and stayed `running` forever.
|
|
36
|
+
//
|
|
37
|
+
// `data` has to be forwarded for the same reason. The sleeper is called
|
|
38
|
+
// with `{ runId, stepId }` and cannot identify the sleeping step
|
|
39
|
+
// without it — it was captured in `delayedTasks` but never passed on.
|
|
40
|
+
// This matches what the BullMQ and pg-boss schedulers already do:
|
|
41
|
+
// enqueue `{ rpcName, data, session }` and invoke the RPC with it.
|
|
42
|
+
const services = getSingletonServices();
|
|
43
|
+
const rpc = rpcService.getContextRPCService(services, {}, false);
|
|
44
|
+
await rpc.invoke(rpcName, data);
|
|
28
45
|
}
|
|
29
46
|
catch (err) {
|
|
30
47
|
getLogger().error(`Failed to execute delayed RPC '${rpcName}': ${err}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/schedule",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
|
+
"description": "In-memory scheduler service for Pikku cron wirings",
|
|
4
5
|
"author": "yasser.fadl@gmail.com",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"module": "dist/index.js",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"prepublishOnly": "yarn build"
|
|
19
20
|
},
|
|
20
21
|
"peerDependencies": {
|
|
21
|
-
"@pikku/core": "^0.12.
|
|
22
|
+
"@pikku/core": "^0.12.80"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^24.11.0",
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { describe, test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { pikkuState } from '@pikku/core/
|
|
3
|
+
import { pikkuState } from '@pikku/core/ecosystem'
|
|
4
|
+
import { addFunction } from '@pikku/core/function'
|
|
4
5
|
|
|
5
6
|
import { InMemorySchedulerService } from './in-memory-scheduler-service.js'
|
|
6
7
|
|
|
8
|
+
const waitFor = async (predicate: () => boolean, withinMs = 2000) => {
|
|
9
|
+
const deadline = Date.now() + withinMs
|
|
10
|
+
while (Date.now() < deadline) {
|
|
11
|
+
if (predicate()) return true
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
describe('InMemorySchedulerService scheduleRPC', () => {
|
|
8
18
|
test('schedules and executes delayed RPC', async () => {
|
|
9
19
|
const logger = {
|
|
@@ -31,4 +41,54 @@ describe('InMemorySchedulerService scheduleRPC', () => {
|
|
|
31
41
|
await scheduler.close()
|
|
32
42
|
}
|
|
33
43
|
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The delay actually has to invoke the RPC, with its data.
|
|
47
|
+
*
|
|
48
|
+
* The test above only proved the task was registered, which is why the
|
|
49
|
+
* scheduler could dispatch through `runScheduledTask` — a lookup against the
|
|
50
|
+
* *cron task* registry that no scheduled RPC is ever in — and still pass.
|
|
51
|
+
* Every internally scheduled RPC failed with ScheduledTaskNotFoundError,
|
|
52
|
+
* `pikkuWorkflowSleeper` among them, so an async workflow never woke from
|
|
53
|
+
* `workflow.sleep`.
|
|
54
|
+
*/
|
|
55
|
+
test('invokes the named RPC with its data once the delay elapses', async () => {
|
|
56
|
+
const errors: string[] = []
|
|
57
|
+
const logger = {
|
|
58
|
+
info: () => {},
|
|
59
|
+
warn: () => {},
|
|
60
|
+
debug: () => {},
|
|
61
|
+
error: (message: string) => errors.push(message),
|
|
62
|
+
}
|
|
63
|
+
pikkuState(null, 'package', 'singletonServices', { logger } as any)
|
|
64
|
+
|
|
65
|
+
const calls: unknown[] = []
|
|
66
|
+
addFunction(
|
|
67
|
+
'delayedTestRpc',
|
|
68
|
+
{
|
|
69
|
+
func: async (_services: any, data: unknown) => calls.push(data),
|
|
70
|
+
} as any,
|
|
71
|
+
null
|
|
72
|
+
)
|
|
73
|
+
pikkuState(null, 'function', 'meta')['delayedTestRpc'] = {
|
|
74
|
+
name: 'delayedTestRpc',
|
|
75
|
+
sessionless: true,
|
|
76
|
+
permissions: [],
|
|
77
|
+
} as any
|
|
78
|
+
|
|
79
|
+
const scheduler = new InMemorySchedulerService()
|
|
80
|
+
try {
|
|
81
|
+
await scheduler.scheduleRPC(20, 'delayedTestRpc', {
|
|
82
|
+
runId: 'run-1',
|
|
83
|
+
stepId: 'step-1',
|
|
84
|
+
})
|
|
85
|
+
assert.ok(
|
|
86
|
+
await waitFor(() => calls.length > 0),
|
|
87
|
+
`RPC was never invoked; scheduler errors: ${JSON.stringify(errors)}`
|
|
88
|
+
)
|
|
89
|
+
assert.deepEqual(calls[0], { runId: 'run-1', stepId: 'step-1' })
|
|
90
|
+
} finally {
|
|
91
|
+
await scheduler.close()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
34
94
|
})
|
|
@@ -5,8 +5,9 @@ import type {
|
|
|
5
5
|
ScheduledTaskSummary,
|
|
6
6
|
} from '@pikku/core'
|
|
7
7
|
import { SchedulerService, parseDurationString } from '@pikku/core'
|
|
8
|
-
import { pikkuState } from '@pikku/core/
|
|
8
|
+
import { pikkuState, getSingletonServices } from '@pikku/core/ecosystem'
|
|
9
9
|
import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
|
|
10
|
+
import { rpcService } from '@pikku/core/rpc'
|
|
10
11
|
|
|
11
12
|
interface DelayedTask {
|
|
12
13
|
taskId: string
|
|
@@ -49,7 +50,27 @@ export class InMemorySchedulerService extends SchedulerService {
|
|
|
49
50
|
const timer = setTimeout(async () => {
|
|
50
51
|
this.delayedTasks.delete(taskId)
|
|
51
52
|
try {
|
|
52
|
-
|
|
53
|
+
// An RPC, not a cron task. `runScheduledTask` looks the name up in the
|
|
54
|
+
// scheduler's task registry, which only ever holds wired cron tasks —
|
|
55
|
+
// so every internally-scheduled RPC died here as
|
|
56
|
+
// ScheduledTaskNotFoundError. The one that matters most is
|
|
57
|
+
// `pikkuWorkflowSleeper`: it is registered by the workflow service as a
|
|
58
|
+
// function, and it is what wakes a run from `workflow.sleep`. With the
|
|
59
|
+
// wake-up lost to a swallowed log line, an async workflow run stopped
|
|
60
|
+
// at its first sleep and stayed `running` forever.
|
|
61
|
+
//
|
|
62
|
+
// `data` has to be forwarded for the same reason. The sleeper is called
|
|
63
|
+
// with `{ runId, stepId }` and cannot identify the sleeping step
|
|
64
|
+
// without it — it was captured in `delayedTasks` but never passed on.
|
|
65
|
+
// This matches what the BullMQ and pg-boss schedulers already do:
|
|
66
|
+
// enqueue `{ rpcName, data, session }` and invoke the RPC with it.
|
|
67
|
+
const services = getSingletonServices()
|
|
68
|
+
const rpc = rpcService.getContextRPCService(
|
|
69
|
+
services as any,
|
|
70
|
+
{},
|
|
71
|
+
false
|
|
72
|
+
) as { invoke: (name: string, data: unknown) => Promise<unknown> }
|
|
73
|
+
await rpc.invoke(rpcName, data)
|
|
53
74
|
} catch (err: unknown) {
|
|
54
75
|
getLogger().error(`Failed to execute delayed RPC '${rpcName}': ${err}`)
|
|
55
76
|
}
|