@live-change/task-service 0.8.31 → 0.8.33

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 (3) hide show
  1. package/model.js +101 -1
  2. package/package.json +4 -4
  3. package/task.js +148 -47
package/model.js CHANGED
@@ -51,6 +51,10 @@ const taskProperties = {
51
51
  },
52
52
  default: []
53
53
  },
54
+ maxRetries: {
55
+ type: Number,
56
+ default: 5
57
+ },
54
58
  progress: {
55
59
  type: Object,
56
60
  properties: {
@@ -82,6 +86,56 @@ const Task = definition.model({
82
86
  },
83
87
  byState: {
84
88
  property: ['state']
89
+ },
90
+ runningRootsByName: {
91
+ property: ['name'],
92
+ function: async function(input, output, { tableName }) {
93
+ function mapFunction(obj) {
94
+ if(!obj) return null
95
+ if(['done', 'failed', 'canceled'].includes(obj.state)) return null
96
+ if(obj.causeType === tableName) return null
97
+ return { id: `"${obj.name}"_${obj.id}`, to: obj.id }
98
+ }
99
+ await input.table(tableName).onChange(async (obj, oldObj) => {
100
+ await output.change(mapFunction(obj), mapFunction(oldObj))
101
+ })
102
+ },
103
+ parameters: {
104
+ tableName: definition.name + '_Task'
105
+ }
106
+ },
107
+ byRoot: {
108
+ function: async function(input, output, { tableName }) {
109
+ async function findAncestors(object){
110
+ const result = []
111
+ let current = object
112
+ while(current) {
113
+ result.push(`"${current.causeType}":"${current.cause}"`)
114
+ current = current.causeType === tableName
115
+ ? await input.table(tableName).object(current.cause).get()
116
+ : null
117
+ }
118
+ //console.log("FOUND ANCESTORS", result, "FOR", object.id)
119
+ return result
120
+ }
121
+ await input.table(tableName).onChange(async (obj, oldObj) => {
122
+ const id = obj?.id || oldObj?.id
123
+ const ancestors = obj ? await findAncestors(obj) : []
124
+ const oldAncestors = oldObj ? await findAncestors(oldObj) : []
125
+ //console.log("ANCESTORS", id, oldAncestors, '=>', ancestors)
126
+ const addedAncestors = ancestors.filter(ancestor => !oldAncestors.includes(ancestor))
127
+ const removedAncestors = oldAncestors.filter(ancestor => !ancestors.includes(ancestor))
128
+ for(const ancestor of addedAncestors) {
129
+ await output.change({ id: `${ancestor}_${id}`, to: id }, null)
130
+ }
131
+ for(const ancestor of removedAncestors) {
132
+ await output.change(null, { id: `${ancestor}_${id}`, to: id })
133
+ }
134
+ })
135
+ },
136
+ parameters: {
137
+ tableName: definition.name + '_Task'
138
+ }
85
139
  }
86
140
  }
87
141
  })
@@ -106,11 +160,35 @@ definition.view({
106
160
  type: Task
107
161
  }
108
162
  },
109
- async daoPath({ hash }) {
163
+ async daoPath({ causeType, cause, hash }) {
110
164
  return Task.indexRangePath('byCauseAndHash', [causeType, cause, hash], { limit: 23 })
111
165
  }
112
166
  })
113
167
 
168
+ definition.view({
169
+ name: 'tasksByRoot',
170
+ properties: {
171
+ rootType: {
172
+ type: String,
173
+ validation: ['nonEmpty']
174
+ },
175
+ root: {
176
+ type: String,
177
+ validation: ['nonEmpty']
178
+ },
179
+ ...App.rangeProperties
180
+ },
181
+ returns: {
182
+ type: Task
183
+ },
184
+ async daoPath(props) {
185
+ const { rootType, root } = props
186
+ const range = App.extractRange(props)
187
+ if(!range.limit) range.limit = 256
188
+ return Task.indexRangePath('byRoot', [rootType, root], range)
189
+ }
190
+ })
191
+
114
192
  definition.view({
115
193
  name: 'task',
116
194
  internal: true,
@@ -126,3 +204,25 @@ definition.view({
126
204
  return Task.path(task)
127
205
  }
128
206
  })
207
+
208
+ definition.view({
209
+ name: 'runningTaskRootsByName',
210
+ internal: true,
211
+ global: true,
212
+ properties: {
213
+ name: {
214
+ type: String,
215
+ validation: ['nonEmpty']
216
+ },
217
+ ...App.rangeProperties
218
+ },
219
+ returns: {
220
+ type: Task
221
+ },
222
+ async daoPath(props) {
223
+ const { name } = props
224
+ const range = App.extractRange(props)
225
+ if(!range.limit) range.limit = 256
226
+ return Task.indexRangePath('runningRootsByName', [name], range)
227
+ }
228
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/task-service",
3
- "version": "0.8.31",
3
+ "version": "0.8.33",
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.8.31",
26
- "@live-change/relations-plugin": "^0.8.31"
25
+ "@live-change/framework": "^0.8.33",
26
+ "@live-change/relations-plugin": "^0.8.33"
27
27
  },
28
- "gitHead": "9ab10c6f2854dd5472cc6becb128951db16218e3"
28
+ "gitHead": "98ff6f9c09e5fc1f408010df6cc8038eff571276"
29
29
  }
package/task.js CHANGED
@@ -3,29 +3,32 @@ const app = App.app()
3
3
 
4
4
  import crypto from 'crypto'
5
5
 
6
+ import PQueue from 'p-queue'
7
+
6
8
  function upperFirst(string) {
7
- return string.charAt(0).toUpperCase() + string.slice(1)
9
+ return string[0].toUpperCase() + string.slice(1)
8
10
  }
9
11
 
10
12
  async function triggerOnTaskStateChange(taskObject, causeType, cause) {
11
- await app.trigger({
13
+ if(!taskObject?.state) throw new Error('Task object state is not defined in ' + taskObject)
14
+ if(!taskObject?.id) throw new Error('Task object id is not defined in '+taskObject)
15
+ await app.trigger({ type: 'task'+upperFirst(taskObject.state), }, {
12
16
  ...taskObject,
13
- type: 'task'+upperFirst(taskObject.state),
14
17
  task: taskObject.id,
15
18
  causeType,
16
19
  cause
17
20
  })
18
- await app.trigger({
21
+ await app.trigger({ type: 'task'+upperFirst(taskObject.name)+upperFirst(taskObject.state) }, {
19
22
  ...taskObject,
20
- type: 'task'+upperFirst(taskObject.name)+upperFirst(taskObject.state),
21
23
  task: taskObject.id,
22
24
  causeType,
23
25
  cause
24
26
  })
25
27
  await app.trigger({
28
+ type: `${taskObject.causeType}_${taskObject.cause}OwnedTask`
29
+ +`${upperFirst(taskObject.name)}${upperFirst(taskObject.state)}`
30
+ }, {
26
31
  ...taskObject,
27
- type: `${taskObject.causeType}_${taksObject.cause}OwnedTask`
28
- +`${upperFirst(taskObject.name)}${upperFirst(taskObject.state)}`,
29
32
  task: taskObject.id,
30
33
  causeType,
31
34
  cause
@@ -33,7 +36,6 @@ async function triggerOnTaskStateChange(taskObject, causeType, cause) {
33
36
  }
34
37
 
35
38
  async function createOrReuseTask(taskDefinition, props, causeType, cause) {
36
-
37
39
  const propertiesJson = JSON.stringify(props)
38
40
  const hash = crypto
39
41
  .createHash('sha256')
@@ -48,60 +50,102 @@ async function createOrReuseTask(taskDefinition, props, causeType, cause) {
48
50
  const oldTask = similarTasks.find(similarTask => similarTask.name === taskDefinition.name
49
51
  && JSON.stringify(similarTask.properties) === propertiesJson)
50
52
 
51
- let taskObject = oldTask
52
- ? await app.serviceViewGet('task', 'task', { task: oldTask.to })
53
+ const taskObject = oldTask
54
+ ? await app.serviceViewGet('task', 'task', { task: oldTask.to || oldTask.id })
53
55
  : {
54
56
  id: app.generateUid(),
55
57
  name: taskDefinition.name,
56
58
  properties: props,
57
59
  hash,
58
- state: 'created'
60
+ state: 'created',
61
+ retries: [],
62
+ maxRetries: taskDefinition.maxRetries ?? 5
59
63
  }
60
64
 
61
65
  if(!oldTask) {
62
66
  /// app.emitEvents
63
- await app.triggerService('task', {
67
+ await app.triggerService({ service: 'task', type: 'task_createCauseOwnedTask' }, {
64
68
  ...taskObject,
65
- type: 'task_createCaseOwnedTask',
66
69
  causeType,
67
70
  cause,
68
71
  task: taskObject.id
69
72
  })
70
73
  await triggerOnTaskStateChange(taskObject, causeType, cause)
71
74
  }
72
-
75
+ return taskObject
73
76
  }
74
77
 
75
78
  async function startTask(taskFunction, props, causeType, cause){
76
- const taskObject = createOrReuseTask(taskFunction.definition, props, causeType, cause)
79
+ const taskObject = await createOrReuseTask(taskFunction.definition, props, causeType, cause)
77
80
  const context = {
78
81
  causeType,
79
82
  cause,
80
- taskObject
83
+ taskObject,
81
84
  }
82
85
  const promise = taskFunction(props, context)
83
- return { task: taskObject.id, taskObject, promise }
86
+ return { task: taskObject.id, taskObject, promise, causeType, cause }
84
87
  }
85
88
 
86
- export default function task(definition) {
87
- const taskFunction = async (props, context, emit) => {
89
+ export default function task(definition, serviceDefinition) {
90
+ if(!definition) throw new Error('Task definition is not defined')
91
+ if(!serviceDefinition) throw new Error('Service definition is not defined')
92
+ const taskFunction = async (props, context, emit, reportProgress = () => {}) => {
88
93
  if(!emit) emit = (events) =>
89
94
  app.emitEvents(definition.name, Array.isArray(events) ? events : [events], {})
90
95
 
91
96
  let taskObject = context.taskObject
92
- ?? await createOrReuseTask(taskDefinition, props, context.causeType, context.cause)
97
+ ?? await createOrReuseTask(definition, props, context.causeType, context.cause)
98
+
99
+ if(!taskObject?.state) throw new Error('Task object state is not defined in ' + taskObject)
100
+ if(!taskObject?.id) throw new Error('Task object id is not defined in '+taskObject)
93
101
 
94
- const maxRetries = definition.maxRetries ?? 5
102
+ const updateQueue = new PQueue({ concurrency: 1 })
95
103
 
96
104
  async function updateTask(data) {
97
- await app.triggerService('task', {
105
+ if(typeof data !== 'object') throw new Error('Task update data is not an object' + JSON.stringify(data))
106
+ if(!taskObject?.state) throw new Error('Task object state is not defined in ' + JSON.stringify(taskObject))
107
+ if(!taskObject?.id) throw new Error('Task object id is not defined in ' + JSON.stringify(taskObject))
108
+ /* console.log("UPDATING TASK", {
98
109
  ...data,
99
- type: 'task_updateCaseOwnedTask',
110
+ type: 'task_updateCauseOwnedTask',
100
111
  causeType: context.causeType,
101
112
  cause: context.cause,
102
113
  task: taskObject.id
103
114
  })
104
- taskObject = await app.serviceViewGet('task', 'task', { task: oldTask.to })
115
+ console.trace("UPDATING TASK!")*/
116
+ await updateQueue.add(async () => {
117
+ const result = await app.triggerService({ service: 'task', type: 'task_updateCauseOwnedTask' }, {
118
+ ...data,
119
+ causeType: context.causeType,
120
+ cause: context.cause,
121
+ task: taskObject.id
122
+ })
123
+
124
+ taskObject = await app.serviceViewGet('task', 'task', { task: taskObject.id })
125
+ //console.log("UPDATED TASK", taskObject, result)
126
+ })
127
+ }
128
+
129
+ let selfProgress = { current: 0, total: 0 }
130
+ const subtasksProgress = []
131
+ let progressUpdateTimer, lastProgressUpdate = 0
132
+ const progressThrottleTime = 400
133
+ function updateProgress() {
134
+ if(progressUpdateTimer) clearTimeout(progressUpdateTimer)
135
+ const current = selfProgress.current + subtasksProgress.reduce(
136
+ (sum, progress) => sum + progress.current * (progress.factor ?? 1), 0)
137
+ const total = selfProgress.total + subtasksProgress.reduce(
138
+ (sum, progress) => sum + progress.total * (progress.factor ?? 1), 0)
139
+ reportProgress(current, total, selfProgress.action)
140
+
141
+ if(lastProgressUpdate + progressThrottleTime > Date.now()) { // ignore this update, do it later
142
+ setTimeout(updateProgress, progressThrottleTime - lastProgressUpdate - Date.now())
143
+ return
144
+ }
145
+ console.log("UPDATE", definition.name, "PROGRESS", current, total, selfProgress, subtasksProgress)
146
+ updateTask({
147
+ progress: { ...selfProgress, current, total }
148
+ })
105
149
  }
106
150
 
107
151
  const runTask = async () => {
@@ -114,18 +158,39 @@ export default function task(definition) {
114
158
  const result = await definition.execute(props, {
115
159
  ...context,
116
160
  task: {
117
- async run(taskFunction, props) {
118
- return await taskFunction(props, {
119
- ...context,
120
- causeType: definition.name + '_Task',
121
- cause: taskObject.id
122
- }, (events) => app.emitEvents(definition.name,
123
- Array.isArray(events) ? events : [events], {}))
161
+ id: taskObject.id,
162
+ async run(taskFunction, props, progressFactor = 1) {
163
+ //console.log("SUBTASK RUN", taskFunction.definition.name, props)
164
+ const subtaskProgress = { current: 0, total: 1, factor: progressFactor }
165
+ subtasksProgress.push(subtaskProgress)
166
+ const result = await taskFunction(
167
+ props,
168
+ {
169
+ ...context,
170
+ taskObject: undefined,
171
+ task: taskObject.id,
172
+ causeType: 'task_Task',
173
+ cause: taskObject.id
174
+ },
175
+ (events) => app.emitEvents(definition.name,
176
+ Array.isArray(events) ? events : [events], {}),
177
+ (current, total, action) => {
178
+ subtaskProgress.current = current
179
+ subtaskProgress.total = total
180
+ updateProgress()
181
+ }
182
+ )
183
+ //console.log("SUBTASK DONE", taskFunction.definition.name, props, '=>', result)
184
+ subtaskProgress.current = subtaskProgress.total
185
+ updateProgress()
186
+ return result
124
187
  },
125
- async progress(current, total) {
126
- await updateTask({
127
- progress: { current, total }
128
- })
188
+ async progress(current, total, action, opts) {
189
+ selfProgress = {
190
+ ...opts,
191
+ current, total, action
192
+ }
193
+ updateProgress()
129
194
  }
130
195
  }
131
196
  })
@@ -136,34 +201,70 @@ export default function task(definition) {
136
201
  })
137
202
  await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
138
203
  } catch(error) {
139
- if(taskObject.retries.length >= maxRetries) {
204
+ console.log("TASK ERROR", error.message, error.stack)
205
+ /*console.log("RETRIES", taskObject.retries?.length, maxRetries)*/
206
+ if((taskObject.retries?.length || 0) >= taskObject.maxRetries) {
140
207
  await updateTask({
141
208
  state: 'failed',
142
209
  doneAt: new Date(),
143
- error: error.message
210
+ error: error.stack ?? error.message ?? error
211
+ })
212
+ } else {
213
+ const retriesCount = (taskObject.retries || []).length
214
+ await updateTask({
215
+ state: 'retrying',
216
+ retries: [...(taskObject.retries || []), {
217
+ startedAt: taskObject.startedAt,
218
+ failedAt: new Date(),
219
+ error: error.stack ?? error.message ?? error
220
+ }]
144
221
  })
145
- await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
222
+ await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retriesCount)))
146
223
  }
147
- await updateTask(taskObject.id, {
148
- state: 'retrying',
149
- retries: [...taskObject.retries, {
150
- startedAt: taskObject.startedAt,
151
- failedAt: new Date(),
152
- error: error.message
153
- }]
154
- })
155
224
  await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
156
225
  }
157
226
  }
158
227
 
159
- /// TODO: implement task queues
160
228
  while(taskObject.state !== 'done' && taskObject.state !== 'failed') {
161
229
  await runTask()
230
+ console.log("TASK", definition.name, "AFTER RUNTASK", taskObject)
162
231
  }
163
232
 
164
233
  return taskObject.result
165
234
  }
166
235
 
236
+ serviceDefinition.beforeStart(async () => {
237
+ setTimeout(async () => {
238
+ let gt = ""
239
+ let tasksToRestart = await app.viewGet('runningTaskRootsByName', {
240
+ name: definition.name,
241
+ gt,
242
+ limit: 25
243
+ })
244
+ while(tasksToRestart.length > 0) {
245
+ console.log("FOUND", tasksToRestart.length, "TASKS", definition.name, "TO RESTART")
246
+ for(const task of tasksToRestart) {
247
+ console.log("RESTARTING TASK", task)
248
+ const taskObject = { ...task, id: task.to ?? task.id }
249
+ const context = {
250
+ causeType: task.causeType,
251
+ cause: task.cause,
252
+ taskObject,
253
+ }
254
+ const promise = taskFunction(taskObject.properties, context)
255
+ /// run async = ignore promise
256
+ await new Promise(resolve => setTimeout(resolve, 1000)) // wait a second
257
+ }
258
+ gt = tasksToRestart[tasksToRestart.length - 1].id
259
+ tasksToRestart = await app.viewGet('runningTaskRootsByName', {
260
+ name: definition.name,
261
+ gt,
262
+ limit: 25
263
+ })
264
+ }
265
+ }, 500)
266
+ })
267
+
167
268
  taskFunction.definition = definition
168
269
  taskFunction.start = async (props, causeType, cause) => {
169
270
  return await startTask(taskFunction, props, causeType, cause)