@live-change/task-service 0.9.85 → 0.9.87

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.
Files changed (4) hide show
  1. package/config.js +11 -0
  2. package/model.js +154 -4
  3. package/package.json +4 -4
  4. package/task.ts +65 -20
package/config.js ADDED
@@ -0,0 +1,11 @@
1
+ import definition from './definition.js'
2
+
3
+ const {
4
+ taskReaderRoles = ['admin', 'owner', 'reader'],
5
+ } = definition.config
6
+
7
+ const config = {
8
+ taskReaderRoles
9
+ }
10
+
11
+ export default config
package/model.js CHANGED
@@ -2,12 +2,17 @@ import App from '@live-change/framework'
2
2
  const app = App.app()
3
3
 
4
4
  import definition from './definition.js'
5
+ import config from './config.js'
5
6
 
6
7
  const taskProperties = {
7
8
  name: {
8
9
  type: String,
9
10
  validation: ['nonEmpty']
10
11
  },
12
+ service: {
13
+ type: String,
14
+ validation: ['nonEmpty']
15
+ },
11
16
  definition: {
12
17
  type: Object
13
18
  },
@@ -21,7 +26,11 @@ const taskProperties = {
21
26
  type: String,
22
27
  },
23
28
  state: {
24
- type: String
29
+ type: String,
30
+ enum: [
31
+ 'created', 'pending', 'running', 'retrying', 'done',
32
+ 'failed', 'canceled', 'fallback', 'fallbackDone', 'interrupted'
33
+ ]
25
34
  },
26
35
  startedAt: {
27
36
  type: Date
@@ -46,6 +55,9 @@ const taskProperties = {
46
55
  },
47
56
  error: {
48
57
  type: String
58
+ },
59
+ stack: {
60
+ type: String
49
61
  }
50
62
  }
51
63
  },
@@ -87,6 +99,84 @@ const Task = definition.model({
87
99
  byState: {
88
100
  property: ['state']
89
101
  },
102
+ byName: {
103
+ property: ['name']
104
+ },
105
+ byNameAndState: {
106
+ property: ['name', 'state']
107
+ },
108
+ independentTasks: {
109
+ function: async function(input, output, { tableName }) {
110
+ const table = await input.table(tableName)
111
+ table.map(async (obj) => {
112
+ if(obj.causeType === tableName) return null
113
+ return { id: obj.id, to: obj.id }
114
+ }).to(output)
115
+ },
116
+ parameters: {
117
+ tableName: definition.name + '_Task'
118
+ }
119
+ },
120
+ independentTasksByName: {
121
+ function: async function(input, output, { tableName }) {
122
+ const table = await input.table(tableName)
123
+ table.map(async (obj) => {
124
+ if(obj.causeType === tableName) return null
125
+ return { id: `"${obj.name}"_${obj.id}`, to: obj.id }
126
+ }).to(output)
127
+ },
128
+ parameters: {
129
+ tableName: definition.name + '_Task'
130
+ }
131
+ },
132
+ independentTasksByState: {
133
+ function: async function(input, output, { tableName }) {
134
+ const table = await input.table(tableName)
135
+ table.map(async (obj) => {
136
+ if(obj.causeType === tableName) return null
137
+ return { id: `"${obj.state}"_${obj.id}`, to: obj.id }
138
+ }).to(output)
139
+ },
140
+ parameters: {
141
+ tableName: definition.name + '_Task'
142
+ }
143
+ },
144
+ independentTasksByNameAndState: {
145
+ function: async function(input, output, { tableName }) {
146
+ const table = await input.table(tableName)
147
+ table.map(async (obj) => {
148
+ if(obj.causeType === tableName) return null
149
+ return { id: `"${obj.name}"_${obj.state}"_${obj.id}`, to: obj.id }
150
+ }).to(output)
151
+ },
152
+ parameters: {
153
+ tableName: definition.name + '_Task'
154
+ }
155
+ },
156
+ taskNames: {
157
+ function: async function(input, output, { indexName }) {
158
+ const index = await input.index(indexName)
159
+ index
160
+ .groupExisting(async (entry) => entry.id.slice(0, entry.id.indexOf('_')+1))
161
+ .map((entry => ({ id: entry.id.slice(1, entry.id.indexOf('_')-1) })))
162
+ .to(output)
163
+ },
164
+ parameters: {
165
+ indexName: definition.name + '_Task_byName'
166
+ }
167
+ },
168
+ independentTaskNames: {
169
+ function: async function(input, output, { indexName }) {
170
+ const index = await input.index(indexName)
171
+ index
172
+ .groupExisting(async (entry) => entry.id.slice(0, entry.id.indexOf('_')+1))
173
+ .map((entry => ({ id: entry.id.slice(1, entry.id.indexOf('_')-1) })))
174
+ .to(output)
175
+ },
176
+ parameters: {
177
+ indexName: definition.name + '_Task_independentTasksByName'
178
+ }
179
+ },
90
180
  runningRootsByName: {
91
181
  property: ['name'],
92
182
  function: async function(input, output, { tableName }) {
@@ -152,6 +242,9 @@ definition.view({
152
242
  },
153
243
  hash: {
154
244
  type: String
245
+ },
246
+ expireDate: {
247
+ type: Date
155
248
  }
156
249
  },
157
250
  returns: {
@@ -161,7 +254,8 @@ definition.view({
161
254
  }
162
255
  },
163
256
  async daoPath({ causeType, cause, hash }) {
164
- return Task.indexRangePath('byCauseAndHash', [causeType, cause, hash], { limit: 23 })
257
+ /// TODO: add expireDate to range
258
+ return Task.indexRangePath('byCauseAndHash', [causeType, cause, hash], { limit: 23, reverse: true })
165
259
  }
166
260
  })
167
261
 
@@ -178,6 +272,16 @@ definition.view({
178
272
  },
179
273
  ...App.rangeProperties
180
274
  },
275
+ accessControl: {
276
+ roles: config.taskReaderRoles,
277
+ objects({ rootType, root }) {
278
+ console.log("OBJECTS", rootType, root)
279
+ return [{
280
+ objectType: rootType,
281
+ object: root
282
+ }]
283
+ }
284
+ },
181
285
  returns: {
182
286
  type: Task
183
287
  },
@@ -189,20 +293,66 @@ definition.view({
189
293
  }
190
294
  })
191
295
 
296
+ definition.view({
297
+ name: 'independentTasks',
298
+ internal: true,
299
+ properties: {
300
+ ...App.rangeProperties,
301
+ name: {
302
+ type: String
303
+ },
304
+ state: {
305
+ type: String
306
+ }
307
+ },
308
+ returns: {
309
+ type: Task
310
+ },
311
+ access: ['admin'],
312
+ async daoPath(props) {
313
+ const range = App.extractRange(props)
314
+ const { name, state } = props
315
+ console.log("PROPS", props)
316
+ const [index, rangePath] = name && state
317
+ ? ['independentTasksByNameAndState', [name, state]]
318
+ : name
319
+ ? ['independentTasksByName', [name]]
320
+ : state
321
+ ? ['independentTasksByState', [state]]
322
+ : ['independentTasks', []]
323
+ return Task.indexRangePath(index, rangePath, range)
324
+ }
325
+ })
326
+
327
+ definition.view({
328
+ name: 'taskNames',
329
+ properties: {
330
+ ...App.rangeProperties
331
+ },
332
+ returns: {
333
+ type: String
334
+ },
335
+ access: ['admin'],
336
+ async daoPath(props) {
337
+ const range = App.extractRange(props)
338
+ return ['database', 'indexRange', app.databaseName, definition.name + '_Task_taskNames', range]
339
+ }
340
+ })
341
+
192
342
  definition.view({
193
343
  name: 'runningTaskRootsByName',
194
344
  internal: true,
195
345
  global: true,
196
346
  properties: {
197
347
  name: {
198
- type: String,
199
- validation: ['nonEmpty']
348
+ type: String
200
349
  },
201
350
  ...App.rangeProperties
202
351
  },
203
352
  returns: {
204
353
  type: Task
205
354
  },
355
+ access: ['admin'],
206
356
  async daoPath(props) {
207
357
  const { name } = props
208
358
  const range = App.extractRange(props)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/task-service",
3
- "version": "0.9.85",
3
+ "version": "0.9.87",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@live-change/framework": "^0.9.85",
26
- "@live-change/relations-plugin": "^0.9.85"
25
+ "@live-change/framework": "^0.9.87",
26
+ "@live-change/relations-plugin": "^0.9.87"
27
27
  },
28
- "gitHead": "126afb0aad3ab6e03aa5742726f429c95c46783a"
28
+ "gitHead": "7a7694ad2801b7ffa16f347aed441ca5f81ab5fd"
29
29
  }
package/task.ts CHANGED
@@ -36,20 +36,25 @@ async function triggerOnTaskStateChange(taskObject, causeType, cause) {
36
36
  })
37
37
  }
38
38
 
39
- async function createOrReuseTask(taskDefinition, props, causeType, cause) {
39
+ async function createOrReuseTask(taskDefinition, props, causeType, cause, expire) {
40
40
  const propertiesJson = JSON.stringify(props)
41
41
  const hash = crypto
42
42
  .createHash('sha256')
43
43
  .update(taskDefinition.name + ':' + propertiesJson)
44
44
  .digest('hex')
45
45
 
46
+ const expireDate = (expire ?? taskDefinition.expire) ? new Date(Date.now() - taskDefinition.expire) : null
47
+
46
48
  const similarTasks = await app.serviceViewGet('task', 'tasksByCauseAndHash', {
47
49
  causeType,
48
50
  cause,
49
- hash
51
+ hash,
52
+ expireDate
50
53
  })
54
+
51
55
  const oldTask = similarTasks.find(similarTask => similarTask.name === taskDefinition.name
52
- && JSON.stringify(similarTask.properties) === propertiesJson)
56
+ && JSON.stringify(similarTask.properties) === propertiesJson
57
+ && (!expireDate || new Date(similarTask.startedAt).getTime() > expireDate.getTime()))
53
58
 
54
59
  const taskObject = oldTask
55
60
  ? await app.serviceViewGet('task', 'task', { task: oldTask.to || oldTask.id })
@@ -59,6 +64,7 @@ async function createOrReuseTask(taskDefinition, props, causeType, cause) {
59
64
  properties: props,
60
65
  hash,
61
66
  state: 'created',
67
+ service: taskDefinition.service,
62
68
  retries: [],
63
69
  maxRetries: taskDefinition.maxRetries ?? 5
64
70
  }
@@ -76,8 +82,8 @@ async function createOrReuseTask(taskDefinition, props, causeType, cause) {
76
82
  return taskObject
77
83
  }
78
84
 
79
- async function startTask(taskFunction, props, causeType, cause){
80
- const taskObject = await createOrReuseTask(taskFunction.definition, props, causeType, cause)
85
+ async function startTask(taskFunction, props, causeType, cause, expire){
86
+ const taskObject = await createOrReuseTask(taskFunction.definition, props, causeType, cause, expire)
81
87
  const context = {
82
88
  causeType,
83
89
  cause,
@@ -125,6 +131,11 @@ interface TaskDefinition {
125
131
  */
126
132
  returns?: Object,
127
133
 
134
+ /**
135
+ * Task expiration time in milliseconds
136
+ */
137
+ expire?: number,
138
+
128
139
  /**
129
140
  * Task execution function
130
141
  * @param props - task properties/parameters
@@ -163,6 +174,11 @@ interface TaskDefinition {
163
174
  */
164
175
  action?: string | true | { name: string } // TODO: create ActionDefinition type
165
176
 
177
+ /**
178
+ * Task service name
179
+ */
180
+ service?: string
181
+
166
182
  }
167
183
 
168
184
  type TaskFunction = (props, context: TaskExecuteContext, emit, reportProgress) => Promise<any>
@@ -170,6 +186,7 @@ type TaskFunction = (props, context: TaskExecuteContext, emit, reportProgress) =
170
186
  export default function task(definition:TaskDefinition, serviceDefinition) {
171
187
  if(!definition) throw new Error('Task definition is not defined')
172
188
  if(!serviceDefinition) throw new Error('Service definition is not defined')
189
+ definition.service = serviceDefinition.name
173
190
  const taskFunction = async (props, context,
174
191
  emit = events => app.emitEvents(definition.name, Array.isArray(events) ? events : [events], {}),
175
192
  reportProgress = (current, total, selfProgress) => {}) => {
@@ -178,8 +195,8 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
178
195
  app.emitEvents(serviceDefinition.name, Array.isArray(events) ? events : [events], {})
179
196
  }
180
197
 
181
- let taskObject = context.taskObject
182
- ?? await createOrReuseTask(definition, props, context.causeType, context.cause)
198
+ let taskObject = context.taskObject ??
199
+ await createOrReuseTask(definition, props, context.causeType, context.cause, context.expire)
183
200
 
184
201
  if(!taskObject?.state) throw new Error('Task object state is not defined in ' + taskObject)
185
202
  if(!taskObject?.id) throw new Error('Task object id is not defined in '+taskObject)
@@ -218,9 +235,11 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
218
235
  function updateProgress() {
219
236
  if(progressUpdateTimer) clearTimeout(progressUpdateTimer)
220
237
  const current = selfProgress.current + subtasksProgress.reduce(
221
- (sum, progress: { current: number, total: number, factor: number }) => sum + progress.current * (progress.factor ?? 1), 0)
238
+ (sum, progress: { current: number, total: number, factor: number }) =>
239
+ sum + progress.current * (progress.factor ?? 1), 0)
222
240
  const total = selfProgress.total + subtasksProgress.reduce(
223
- (sum, progress: { current: number, total: number, factor: number }) => sum + progress.total * (progress.factor ?? 1), 0)
241
+ (sum, progress: { current: number, total: number, factor: number }) =>
242
+ sum + progress.total * (progress.factor ?? 1), 0)
224
243
  reportProgress(current, total, selfProgress.action)
225
244
 
226
245
  if(lastProgressUpdate + progressThrottleTime > Date.now()) { // ignore this update, do it later
@@ -243,7 +262,7 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
243
262
  ...context,
244
263
  task: {
245
264
  id: taskObject.id,
246
- async run(taskFunction: TaskFunction, props, progressFactor = 1) {
265
+ async run(taskFunction: TaskFunction, props, progressFactor = 1, expire = undefined) {
247
266
  if(typeof taskFunction !== 'function') {
248
267
  console.log("TASK FUNCTION", taskFunction)
249
268
  throw new Error('Task function is not a function')
@@ -258,7 +277,8 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
258
277
  taskObject: undefined,
259
278
  task: taskObject.id,
260
279
  causeType: 'task_Task',
261
- cause: taskObject.id
280
+ cause: taskObject.id,
281
+ expire
262
282
  },
263
283
  (events) => app.emitEvents(serviceDefinition.name,
264
284
  Array.isArray(events) ? events : [events], {}),
@@ -307,20 +327,21 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
307
327
  } catch(error) {
308
328
  console.error("TASK ERROR", error.message, error.stack)
309
329
  /*console.log("RETRIES", taskObject.retries?.length, maxRetries)*/
310
- if((taskObject.retries?.length || 0) >= taskObject.maxRetries - 1) {
330
+ if((taskObject.retries?.length || 0) >= taskObject.maxRetries - 1 || error.taskNoRetry) {
311
331
  await updateTask({
312
- state: definition.fallback ? 'fallback' : 'failed',
332
+ state: (definition.fallback && !error.taskNoFallback) ? 'fallback' : 'failed',
313
333
  doneAt: new Date(),
314
334
  error: /*error.stack ??*/ error.message ?? error,
315
335
  retries: [...(taskObject.retries || []), {
316
336
  startedAt: taskObject.startedAt,
317
337
  failedAt: new Date(),
318
- error: /*error.stack ??*/ error.message ?? error
338
+ error: /*error.stack ??*/ error.message ?? error,
339
+ stack: error.stack
319
340
  }]
320
341
  })
321
342
  console.error("TASK", taskObject.id, "OF TYPE", definition.name,
322
343
  "WITH PARAMETERS", props, "FAILED WITH ERROR", error.stack ?? error.message ?? error)
323
- if(definition.fallback) {
344
+ if(definition.fallback && !error.taskNoFallback) {
324
345
  await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
325
346
  let result
326
347
  if(typeof definition.fallback !== 'function') {
@@ -343,7 +364,8 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
343
364
  retries: [...(taskObject.retries || []), {
344
365
  startedAt: taskObject.startedAt,
345
366
  failedAt: new Date(),
346
- error:/* error.stack ?? */error.message ?? error
367
+ error: error.message ?? error,
368
+ stack: error.stack
347
369
  }]
348
370
  })
349
371
  await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retriesCount)))
@@ -368,6 +390,9 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
368
390
  return taskObject.result
369
391
  }
370
392
 
393
+ serviceDefinition.tasks = serviceDefinition.tasks || {}
394
+ serviceDefinition.tasks[definition.name] = definition
395
+
371
396
  serviceDefinition.afterStart(async () => {
372
397
  setTimeout(async () => {
373
398
  let gt = undefined
@@ -386,6 +411,22 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
386
411
  cause: task.cause,
387
412
  taskObject,
388
413
  }
414
+ /// mark started subtasks as interrupted
415
+ const subtasks = await app.serviceViewGet('task', 'tasksByRoot', {
416
+ rootType: 'task_Task',
417
+ root: taskObject.to ?? taskObject.id
418
+ })
419
+ console.log("SUBTASKS", subtasks)
420
+ for(const subtask of subtasks) {
421
+ if(subtask.state === 'running' && (subtask.to ?? subtask.id) !== (taskObject.to ?? taskObject.id)) {
422
+ await app.triggerService({ service: 'task', type: 'task_updateTask' }, {
423
+ task: subtask.to ?? subtask.id,
424
+ causeType: subtask.causeType,
425
+ cause: subtask.cause,
426
+ state: 'interrupted'
427
+ })
428
+ }
429
+ }
389
430
  const promise = taskFunction(taskObject.properties, context)
390
431
  /// run async = ignore promise
391
432
  await new Promise(resolve => setTimeout(resolve, 1000)) // wait a second
@@ -413,7 +454,7 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
413
454
  },
414
455
  async execute(props, context, emit) {
415
456
  const startResult =
416
- await startTask(taskFunction, props, 'trigger', context.reaction.id)
457
+ await startTask(taskFunction, props, 'trigger', context.reaction.id, props.taskExpire)
417
458
  return startResult.task
418
459
  }
419
460
  })
@@ -433,8 +474,12 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
433
474
  type: Task
434
475
  },
435
476
  async execute(props, context, emit) {
477
+ const client = {
478
+ ...context.client,
479
+ sessionKey: undefined
480
+ }
436
481
  const startResult =
437
- await startTask(taskFunction, { ...props, client: context.client }, 'command', context.command.id)
482
+ await startTask(taskFunction, { ...props, client }, 'command', context.command.id, undefined)
438
483
  return startResult.task
439
484
  },
440
485
  ...(typeof definition.action === 'object' && definition.action)
@@ -442,8 +487,8 @@ export default function task(definition:TaskDefinition, serviceDefinition) {
442
487
  }
443
488
 
444
489
  taskFunction.definition = definition
445
- taskFunction.start = async (props, causeType, cause) => {
446
- return await startTask(taskFunction, props, causeType, cause)
490
+ taskFunction.start = async (props, causeType, cause, expire = undefined) => {
491
+ return await startTask(taskFunction, props, causeType, cause, expire)
447
492
  }
448
493
  return taskFunction
449
494