@motiadev/adapter-redis-cron 0.8.2-beta.140-709523
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 +16 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/redis-cron-adapter.d.ts +24 -0
- package/dist/redis-cron-adapter.d.ts.map +1 -0
- package/dist/redis-cron-adapter.js +236 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +24 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2025-10-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release of Redis cron adapter for Motia
|
|
7
|
+
- Distributed locking to prevent duplicate cron job executions
|
|
8
|
+
- Automatic TTL for lock expiration
|
|
9
|
+
- Lock renewal support for long-running jobs
|
|
10
|
+
- Health check functionality
|
|
11
|
+
- Configurable retry logic for lock acquisition
|
|
12
|
+
- Instance tracking for monitoring
|
|
13
|
+
- Graceful shutdown with automatic lock cleanup
|
|
14
|
+
- Full TypeScript support with type definitions
|
|
15
|
+
- Comprehensive documentation and examples
|
|
16
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Motia
|
|
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
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# @motiadev/adapter-redis-cron
|
|
2
|
+
|
|
3
|
+
Redis cron adapter for Motia framework, enabling distributed cron job coordination to prevent duplicate executions across multiple instances.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @motiadev/adapter-redis-cron
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Configure the Redis cron adapter in your `motia.config.ts`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { config } from '@motiadev/core'
|
|
17
|
+
import { RedisCronAdapter } from '@motiadev/adapter-redis-cron'
|
|
18
|
+
|
|
19
|
+
export default config({
|
|
20
|
+
adapters: {
|
|
21
|
+
cron: new RedisCronAdapter({
|
|
22
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
23
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
24
|
+
password: process.env.REDIS_PASSWORD,
|
|
25
|
+
keyPrefix: 'motia:cron:lock:',
|
|
26
|
+
lockTTL: 300000,
|
|
27
|
+
instanceId: process.env.INSTANCE_ID || undefined,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration Options
|
|
34
|
+
|
|
35
|
+
### RedisCronAdapterConfig
|
|
36
|
+
|
|
37
|
+
| Option | Type | Default | Description |
|
|
38
|
+
|--------|------|---------|-------------|
|
|
39
|
+
| `host` | `string` | `'localhost'` | Redis server host |
|
|
40
|
+
| `port` | `number` | `6379` | Redis server port |
|
|
41
|
+
| `password` | `string` | `undefined` | Redis authentication password |
|
|
42
|
+
| `username` | `string` | `undefined` | Redis authentication username |
|
|
43
|
+
| `database` | `number` | `0` | Redis database number |
|
|
44
|
+
| `keyPrefix` | `string` | `'motia:cron:lock:'` | Prefix for all lock keys |
|
|
45
|
+
| `lockTTL` | `number` | `300000` | Lock time-to-live in milliseconds (5 minutes) |
|
|
46
|
+
| `lockRetryDelay` | `number` | `1000` | Delay between lock retry attempts in milliseconds |
|
|
47
|
+
| `lockRetryAttempts` | `number` | `0` | Number of times to retry acquiring a lock |
|
|
48
|
+
| `instanceId` | `string` | Auto-generated UUID | Unique identifier for this instance |
|
|
49
|
+
| `enableHealthCheck` | `boolean` | `true` | Whether to perform periodic health checks |
|
|
50
|
+
| `socket.reconnectStrategy` | `function` | Auto-retry | Custom reconnection strategy |
|
|
51
|
+
| `socket.connectTimeout` | `number` | `10000` | Connection timeout in milliseconds |
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
When running multiple instances of a Motia application, each instance schedules the same cron jobs. Without coordination, this leads to duplicate executions. The Redis Cron Adapter solves this using distributed locking:
|
|
56
|
+
|
|
57
|
+
1. **Job Scheduling**: All instances schedule cron jobs normally
|
|
58
|
+
2. **Lock Acquisition**: When a cron job triggers, the instance attempts to acquire a distributed lock
|
|
59
|
+
3. **Execution**: Only the instance that successfully acquires the lock executes the job
|
|
60
|
+
4. **Lock Release**: After execution completes (or fails), the lock is released
|
|
61
|
+
5. **TTL Protection**: Locks have a TTL to prevent deadlocks if an instance crashes
|
|
62
|
+
|
|
63
|
+
## Execution Flow
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Instance 1 Instance 2 Instance 3
|
|
67
|
+
| | |
|
|
68
|
+
| Cron triggers | Cron triggers | Cron triggers
|
|
69
|
+
| (9:00 AM) | (9:00 AM) | (9:00 AM)
|
|
70
|
+
| | |
|
|
71
|
+
v v v
|
|
72
|
+
acquireLock() acquireLock() acquireLock()
|
|
73
|
+
| | |
|
|
74
|
+
v v v
|
|
75
|
+
┌─────────────────────────────────────────────────────┐
|
|
76
|
+
│ Distributed Lock Store (Redis) │
|
|
77
|
+
│ │
|
|
78
|
+
│ Lock: daily-report │
|
|
79
|
+
│ Owner: instance-1 │
|
|
80
|
+
│ Acquired: 2025-10-22 09:00:00 │
|
|
81
|
+
│ Expires: 2025-10-22 09:05:00 │
|
|
82
|
+
└─────────────────────────────────────────────────────┘
|
|
83
|
+
| | |
|
|
84
|
+
v v v
|
|
85
|
+
Lock acquired ✓ Lock failed ✗ Lock failed ✗
|
|
86
|
+
| | |
|
|
87
|
+
v | |
|
|
88
|
+
Execute job Skip execution Skip execution
|
|
89
|
+
| | |
|
|
90
|
+
v | |
|
|
91
|
+
releaseLock() | |
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
- **Distributed Locking**: Prevents duplicate cron job executions across instances
|
|
97
|
+
- **Automatic TTL**: Locks expire automatically to prevent deadlocks
|
|
98
|
+
- **Lock Renewal**: Support for renewing locks for long-running jobs
|
|
99
|
+
- **Health Checks**: Monitor Redis connection health
|
|
100
|
+
- **Retry Logic**: Configurable retry attempts for lock acquisition
|
|
101
|
+
- **Instance Tracking**: Track which instance holds each lock
|
|
102
|
+
- **Graceful Shutdown**: Automatically releases locks on shutdown
|
|
103
|
+
|
|
104
|
+
## Key Namespacing
|
|
105
|
+
|
|
106
|
+
The adapter uses the following key pattern:
|
|
107
|
+
```
|
|
108
|
+
{keyPrefix}{jobName}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For example:
|
|
112
|
+
```
|
|
113
|
+
motia:cron:lock:daily-report
|
|
114
|
+
motia:cron:lock:cleanup-task
|
|
115
|
+
motia:cron:lock:send-notifications
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Example Cron Step
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// steps/dailyReport/dailyReport.step.ts
|
|
122
|
+
import { type Handlers } from './types'
|
|
123
|
+
|
|
124
|
+
export const config = {
|
|
125
|
+
name: 'DailyReport',
|
|
126
|
+
cron: '0 9 * * *', // Run at 9 AM daily
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const handler: Handlers['DailyReport'] = async ({ logger }) => {
|
|
130
|
+
logger.info('Generating daily report')
|
|
131
|
+
|
|
132
|
+
// This will only execute on ONE instance
|
|
133
|
+
// even if you have 10 instances running
|
|
134
|
+
|
|
135
|
+
await generateReport()
|
|
136
|
+
await sendReport()
|
|
137
|
+
|
|
138
|
+
logger.info('Daily report sent successfully')
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Lock Renewal for Long-Running Jobs
|
|
143
|
+
|
|
144
|
+
For cron jobs that may take longer than the lock TTL, implement lock renewal:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
export const handler: Handlers['LongRunningJob'] = async ({ logger }) => {
|
|
148
|
+
// Note: Lock renewal is handled internally by the cron handler
|
|
149
|
+
// You can configure a longer lockTTL in the adapter config
|
|
150
|
+
|
|
151
|
+
logger.info('Starting long-running job')
|
|
152
|
+
|
|
153
|
+
// Your long-running logic here
|
|
154
|
+
await processLargeDataset()
|
|
155
|
+
|
|
156
|
+
logger.info('Long-running job completed')
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Monitoring Active Locks
|
|
161
|
+
|
|
162
|
+
To monitor which instances are executing cron jobs:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { RedisCronAdapter } from '@motiadev/adapter-redis-cron'
|
|
166
|
+
|
|
167
|
+
const adapter = new RedisCronAdapter({
|
|
168
|
+
host: 'localhost',
|
|
169
|
+
port: 6379,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const activeLocks = await adapter.getActiveLocks()
|
|
173
|
+
console.log('Active cron jobs:', activeLocks)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Environment Variables
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
REDIS_HOST=localhost
|
|
180
|
+
REDIS_PORT=6379
|
|
181
|
+
REDIS_PASSWORD=your-password
|
|
182
|
+
REDIS_DATABASE=0
|
|
183
|
+
CRON_KEY_PREFIX=motia:cron:lock:
|
|
184
|
+
CRON_LOCK_TTL=300000
|
|
185
|
+
INSTANCE_ID=instance-1
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Lock TTL Selection
|
|
189
|
+
|
|
190
|
+
Choose the lock TTL based on your job execution times:
|
|
191
|
+
|
|
192
|
+
- **Short Jobs** (< 1 minute): Use 60000ms (1 minute) TTL
|
|
193
|
+
- **Medium Jobs** (1-5 minutes): Use 300000ms (5 minutes) TTL
|
|
194
|
+
- **Long Jobs** (> 5 minutes): Use 600000ms+ (10+ minutes) TTL
|
|
195
|
+
|
|
196
|
+
## Performance Considerations
|
|
197
|
+
|
|
198
|
+
- **Lock Overhead**: Redis lock acquisition adds ~2-5ms overhead per cron trigger (negligible)
|
|
199
|
+
- **Scalability**: Scales linearly with number of unique cron jobs, not number of instances
|
|
200
|
+
- **Network Latency**: Consider network latency between app and Redis
|
|
201
|
+
- **TTL Settings**: Set appropriate TTL values based on job execution times
|
|
202
|
+
|
|
203
|
+
## Edge Cases
|
|
204
|
+
|
|
205
|
+
### Instance Crashes
|
|
206
|
+
|
|
207
|
+
If an instance crashes while holding a lock, the lock will expire after the TTL, allowing another instance to execute the job. This provides automatic recovery.
|
|
208
|
+
|
|
209
|
+
### Clock Skew
|
|
210
|
+
|
|
211
|
+
Different instances might have slightly different system clocks, causing cron jobs to trigger at slightly different times. The lock mechanism ensures only one execution regardless of timing differences.
|
|
212
|
+
|
|
213
|
+
### Redis Unavailability
|
|
214
|
+
|
|
215
|
+
If Redis is unavailable, all instances will fail to acquire locks and no cron jobs will execute. This is a fail-safe behavior to prevent duplicates.
|
|
216
|
+
|
|
217
|
+
## Development vs Production
|
|
218
|
+
|
|
219
|
+
### Development
|
|
220
|
+
|
|
221
|
+
For development environments where horizontal scaling is not needed:
|
|
222
|
+
- Omit the `cron` adapter from configuration
|
|
223
|
+
- Cron jobs will execute normally without distributed locking
|
|
224
|
+
- Reduces external dependencies during development
|
|
225
|
+
|
|
226
|
+
### Production
|
|
227
|
+
|
|
228
|
+
For production with multiple instances:
|
|
229
|
+
- Always configure a cron adapter
|
|
230
|
+
- Use managed Redis services (AWS ElastiCache, etc.) for high availability
|
|
231
|
+
- Set appropriate lock TTLs based on job execution times
|
|
232
|
+
- Monitor active locks to ensure proper coordination
|
|
233
|
+
|
|
234
|
+
## Troubleshooting
|
|
235
|
+
|
|
236
|
+
### Cron Jobs Not Executing
|
|
237
|
+
|
|
238
|
+
If no cron jobs are executing:
|
|
239
|
+
1. Verify Redis is accessible from all instances
|
|
240
|
+
2. Check Redis connection credentials
|
|
241
|
+
3. Review instance logs for lock acquisition errors
|
|
242
|
+
4. Verify cron expressions are valid
|
|
243
|
+
5. Check Redis memory and connection limits
|
|
244
|
+
|
|
245
|
+
### Duplicate Executions
|
|
246
|
+
|
|
247
|
+
If you see duplicate executions:
|
|
248
|
+
1. Verify all instances use the same Redis instance
|
|
249
|
+
2. Check that cron adapter is properly configured
|
|
250
|
+
3. Review lock TTL settings (may be too short)
|
|
251
|
+
4. Check for clock skew between instances
|
|
252
|
+
5. Monitor Redis connection stability
|
|
253
|
+
|
|
254
|
+
### Lock Contention
|
|
255
|
+
|
|
256
|
+
If locks are frequently contended:
|
|
257
|
+
1. Review cron schedules to avoid overlapping jobs
|
|
258
|
+
2. Consider staggering job execution times
|
|
259
|
+
3. Increase lock retry attempts if appropriate
|
|
260
|
+
4. Monitor Redis performance
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
|
265
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisCronAdapter = void 0;
|
|
4
|
+
var redis_cron_adapter_1 = require("./redis-cron-adapter");
|
|
5
|
+
Object.defineProperty(exports, "RedisCronAdapter", { enumerable: true, get: function () { return redis_cron_adapter_1.RedisCronAdapter; } });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CronAdapter, CronLock, CronLockInfo } from '@motiadev/core';
|
|
2
|
+
import type { RedisCronAdapterConfig } from './types';
|
|
3
|
+
export declare class RedisCronAdapter implements CronAdapter {
|
|
4
|
+
private client;
|
|
5
|
+
private keyPrefix;
|
|
6
|
+
private lockTTL;
|
|
7
|
+
private lockRetryDelay;
|
|
8
|
+
private lockRetryAttempts;
|
|
9
|
+
private instanceId;
|
|
10
|
+
private enableHealthCheck;
|
|
11
|
+
private connected;
|
|
12
|
+
constructor(config: RedisCronAdapterConfig);
|
|
13
|
+
private connect;
|
|
14
|
+
private ensureConnected;
|
|
15
|
+
private makeKey;
|
|
16
|
+
acquireLock(jobName: string, ttl?: number): Promise<CronLock | null>;
|
|
17
|
+
releaseLock(lock: CronLock): Promise<void>;
|
|
18
|
+
renewLock(lock: CronLock, ttl: number): Promise<boolean>;
|
|
19
|
+
isHealthy(): Promise<boolean>;
|
|
20
|
+
shutdown(): Promise<void>;
|
|
21
|
+
getActiveLocks(): Promise<CronLockInfo[]>;
|
|
22
|
+
private scanKeys;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=redis-cron-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-cron-adapter.d.ts","sourceRoot":"","sources":["../src/redis-cron-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AAErD,qBAAa,gBAAiB,YAAW,WAAW;IAClD,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,SAAS,CAAQ;gBAEb,MAAM,EAAE,sBAAsB;YA+C5B,OAAO;YAWP,eAAe;IAM7B,OAAO,CAAC,OAAO;IAIT,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IA8CpE,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B1C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwCxD,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAc7B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBzB,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;YA2BjC,QAAQ;CAevB"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisCronAdapter = void 0;
|
|
4
|
+
const redis_1 = require("redis");
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
class RedisCronAdapter {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.connected = false;
|
|
9
|
+
this.keyPrefix = config.keyPrefix || 'motia:cron:lock:';
|
|
10
|
+
this.lockTTL = config.lockTTL || 300000;
|
|
11
|
+
this.lockRetryDelay = config.lockRetryDelay || 1000;
|
|
12
|
+
this.lockRetryAttempts = config.lockRetryAttempts || 0;
|
|
13
|
+
this.instanceId = config.instanceId || `motia-${(0, uuid_1.v4)()}`;
|
|
14
|
+
this.enableHealthCheck = config.enableHealthCheck ?? true;
|
|
15
|
+
this.client = (0, redis_1.createClient)({
|
|
16
|
+
socket: {
|
|
17
|
+
host: config.host || 'localhost',
|
|
18
|
+
port: config.port || 6379,
|
|
19
|
+
reconnectStrategy: config.socket?.reconnectStrategy ||
|
|
20
|
+
((retries) => {
|
|
21
|
+
if (retries > 10) {
|
|
22
|
+
return new Error('Redis connection retry limit exceeded');
|
|
23
|
+
}
|
|
24
|
+
return Math.min(retries * 100, 3000);
|
|
25
|
+
}),
|
|
26
|
+
connectTimeout: config.socket?.connectTimeout || 10000,
|
|
27
|
+
},
|
|
28
|
+
password: config.password,
|
|
29
|
+
username: config.username,
|
|
30
|
+
database: config.database || 0,
|
|
31
|
+
});
|
|
32
|
+
this.client.on('error', (err) => {
|
|
33
|
+
console.error('[Redis Cron] Client error:', err);
|
|
34
|
+
});
|
|
35
|
+
this.client.on('connect', () => {
|
|
36
|
+
this.connected = true;
|
|
37
|
+
});
|
|
38
|
+
this.client.on('disconnect', () => {
|
|
39
|
+
console.warn('[Redis Cron] Disconnected');
|
|
40
|
+
this.connected = false;
|
|
41
|
+
});
|
|
42
|
+
this.client.on('reconnecting', () => {
|
|
43
|
+
console.log('[Redis Cron] Reconnecting...');
|
|
44
|
+
});
|
|
45
|
+
this.connect();
|
|
46
|
+
}
|
|
47
|
+
async connect() {
|
|
48
|
+
if (!this.connected && !this.client.isOpen) {
|
|
49
|
+
try {
|
|
50
|
+
await this.client.connect();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('[Redis Cron] Failed to connect:', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async ensureConnected() {
|
|
59
|
+
if (!this.client.isOpen) {
|
|
60
|
+
await this.connect();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
makeKey(jobName) {
|
|
64
|
+
return `${this.keyPrefix}${jobName}`;
|
|
65
|
+
}
|
|
66
|
+
async acquireLock(jobName, ttl) {
|
|
67
|
+
await this.ensureConnected();
|
|
68
|
+
const lockTTL = ttl || this.lockTTL;
|
|
69
|
+
const lockId = (0, uuid_1.v4)();
|
|
70
|
+
const key = this.makeKey(jobName);
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const expiresAt = now + lockTTL;
|
|
73
|
+
const lock = {
|
|
74
|
+
jobName,
|
|
75
|
+
lockId,
|
|
76
|
+
acquiredAt: now,
|
|
77
|
+
expiresAt,
|
|
78
|
+
instanceId: this.instanceId,
|
|
79
|
+
};
|
|
80
|
+
const lockData = JSON.stringify(lock);
|
|
81
|
+
const result = await this.client.set(key, lockData, {
|
|
82
|
+
PX: lockTTL,
|
|
83
|
+
NX: true,
|
|
84
|
+
});
|
|
85
|
+
if (result === 'OK') {
|
|
86
|
+
return lock;
|
|
87
|
+
}
|
|
88
|
+
if (this.lockRetryAttempts > 0) {
|
|
89
|
+
for (let attempt = 0; attempt < this.lockRetryAttempts; attempt++) {
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, this.lockRetryDelay));
|
|
91
|
+
const retryResult = await this.client.set(key, lockData, {
|
|
92
|
+
PX: lockTTL,
|
|
93
|
+
NX: true,
|
|
94
|
+
});
|
|
95
|
+
if (retryResult === 'OK') {
|
|
96
|
+
return lock;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
async releaseLock(lock) {
|
|
103
|
+
await this.ensureConnected();
|
|
104
|
+
const key = this.makeKey(lock.jobName);
|
|
105
|
+
const luaScript = `
|
|
106
|
+
local current = redis.call('GET', KEYS[1])
|
|
107
|
+
if not current then
|
|
108
|
+
return 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
local lock = cjson.decode(current)
|
|
112
|
+
if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
|
|
113
|
+
return redis.call('DEL', KEYS[1])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
return 0
|
|
117
|
+
`;
|
|
118
|
+
try {
|
|
119
|
+
await this.client.eval(luaScript, {
|
|
120
|
+
keys: [key],
|
|
121
|
+
arguments: [lock.lockId, lock.instanceId],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error('[Redis Cron] Error releasing lock:', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async renewLock(lock, ttl) {
|
|
129
|
+
await this.ensureConnected();
|
|
130
|
+
const key = this.makeKey(lock.jobName);
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const expiresAt = now + ttl;
|
|
133
|
+
const renewedLock = {
|
|
134
|
+
...lock,
|
|
135
|
+
expiresAt,
|
|
136
|
+
};
|
|
137
|
+
const luaScript = `
|
|
138
|
+
local current = redis.call('GET', KEYS[1])
|
|
139
|
+
if not current then
|
|
140
|
+
return 0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local lock = cjson.decode(current)
|
|
144
|
+
if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
|
|
145
|
+
redis.call('SET', KEYS[1], ARGV[3], 'PX', ARGV[4])
|
|
146
|
+
return 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
`;
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.client.eval(luaScript, {
|
|
153
|
+
keys: [key],
|
|
154
|
+
arguments: [lock.lockId, lock.instanceId, JSON.stringify(renewedLock), ttl.toString()],
|
|
155
|
+
});
|
|
156
|
+
return result === 1;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error('[Redis Cron] Error renewing lock:', error);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async isHealthy() {
|
|
164
|
+
if (!this.enableHealthCheck) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await this.ensureConnected();
|
|
169
|
+
const result = await this.client.ping();
|
|
170
|
+
return result === 'PONG';
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async shutdown() {
|
|
177
|
+
await this.ensureConnected();
|
|
178
|
+
const pattern = `${this.keyPrefix}*`;
|
|
179
|
+
const keys = await this.scanKeys(pattern);
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
const lockData = await this.client.get(key);
|
|
182
|
+
if (lockData) {
|
|
183
|
+
try {
|
|
184
|
+
const lock = JSON.parse(lockData);
|
|
185
|
+
if (lock.instanceId === this.instanceId) {
|
|
186
|
+
await this.client.del(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error('[Redis Cron] Error cleaning up lock during shutdown:', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (this.client.isOpen) {
|
|
195
|
+
await this.client.quit();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async getActiveLocks() {
|
|
199
|
+
await this.ensureConnected();
|
|
200
|
+
const pattern = `${this.keyPrefix}*`;
|
|
201
|
+
const keys = await this.scanKeys(pattern);
|
|
202
|
+
const locks = [];
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
const lockData = await this.client.get(key);
|
|
205
|
+
if (lockData) {
|
|
206
|
+
try {
|
|
207
|
+
const lock = JSON.parse(lockData);
|
|
208
|
+
locks.push({
|
|
209
|
+
jobName: lock.jobName,
|
|
210
|
+
instanceId: lock.instanceId,
|
|
211
|
+
acquiredAt: lock.acquiredAt,
|
|
212
|
+
expiresAt: lock.expiresAt,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error('[Redis Cron] Error parsing lock data:', error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return locks;
|
|
221
|
+
}
|
|
222
|
+
async scanKeys(pattern) {
|
|
223
|
+
const keys = [];
|
|
224
|
+
let cursor = 0;
|
|
225
|
+
do {
|
|
226
|
+
const result = await this.client.scan(cursor, {
|
|
227
|
+
MATCH: pattern,
|
|
228
|
+
COUNT: 100,
|
|
229
|
+
});
|
|
230
|
+
cursor = result.cursor;
|
|
231
|
+
keys.push(...result.keys);
|
|
232
|
+
} while (cursor !== 0);
|
|
233
|
+
return keys;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
exports.RedisCronAdapter = RedisCronAdapter;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RedisCronAdapterConfig {
|
|
2
|
+
host?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
password?: string;
|
|
5
|
+
username?: string;
|
|
6
|
+
database?: number;
|
|
7
|
+
keyPrefix?: string;
|
|
8
|
+
lockTTL?: number;
|
|
9
|
+
lockRetryDelay?: number;
|
|
10
|
+
lockRetryAttempts?: number;
|
|
11
|
+
instanceId?: string;
|
|
12
|
+
enableHealthCheck?: boolean;
|
|
13
|
+
socket?: {
|
|
14
|
+
reconnectStrategy?: (retries: number) => number | Error;
|
|
15
|
+
connectTimeout?: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE;QACP,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,CAAA;QACvD,cAAc,CAAC,EAAE,MAAM,CAAA;KACxB,CAAA;CACF"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@motiadev/adapter-redis-cron",
|
|
3
|
+
"description": "Redis cron adapter for Motia framework, enabling distributed cron job coordination to prevent duplicate executions across multiple instances.",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"version": "0.8.2-beta.140-709523",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"redis": "^4.7.0",
|
|
9
|
+
"uuid": "^11.1.0",
|
|
10
|
+
"@motiadev/core": "0.8.2-beta.140-709523"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^22.10.2",
|
|
14
|
+
"typescript": "^5.7.2"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@motiadev/core": "^0.8.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rm -rf dist && tsc",
|
|
21
|
+
"lint": "biome check .",
|
|
22
|
+
"watch": "tsc --watch"
|
|
23
|
+
}
|
|
24
|
+
}
|