@live-change/task-service 0.8.32 → 0.8.34

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 +100 -0
  2. package/package.json +4 -4
  3. package/task.js +131 -30
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
  })
@@ -111,6 +165,30 @@ definition.view({
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.sortedIndexRangePath('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.32",
3
+ "version": "0.8.34",
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.32",
26
- "@live-change/relations-plugin": "^0.8.32"
25
+ "@live-change/framework": "^0.8.34",
26
+ "@live-change/relations-plugin": "^0.8.34"
27
27
  },
28
- "gitHead": "9ea7767670a99404794087726223064c45d798d3"
28
+ "gitHead": "40e61928bf43b35352c76fc135f36a2d8bd76c4a"
29
29
  }
package/task.js CHANGED
@@ -3,6 +3,8 @@ 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
9
  return string[0].toUpperCase() + string.slice(1)
8
10
  }
@@ -49,14 +51,15 @@ async function createOrReuseTask(taskDefinition, props, causeType, cause) {
49
51
  && JSON.stringify(similarTask.properties) === propertiesJson)
50
52
 
51
53
  const taskObject = oldTask
52
- ? await app.serviceViewGet('task', 'task', { task: oldTask.to })
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
60
  state: 'created',
59
- retries: []
61
+ retries: [],
62
+ maxRetries: taskDefinition.maxRetries ?? 5
60
63
  }
61
64
 
62
65
  if(!oldTask) {
@@ -77,24 +80,26 @@ async function startTask(taskFunction, 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)
93
98
 
94
99
  if(!taskObject?.state) throw new Error('Task object state is not defined in ' + taskObject)
95
100
  if(!taskObject?.id) throw new Error('Task object id is not defined in '+taskObject)
96
101
 
97
- const maxRetries = definition.maxRetries ?? 5
102
+ const updateQueue = new PQueue({ concurrency: 1 })
98
103
 
99
104
  async function updateTask(data) {
100
105
  if(typeof data !== 'object') throw new Error('Task update data is not an object' + JSON.stringify(data))
@@ -108,14 +113,39 @@ export default function task(definition) {
108
113
  task: taskObject.id
109
114
  })
110
115
  console.trace("UPDATING TASK!")*/
111
- const result = await app.triggerService({ service: 'task', type: 'task_updateCauseOwnedTask' }, {
112
- ...data,
113
- causeType: context.causeType,
114
- cause: context.cause,
115
- task: taskObject.id
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 }
116
148
  })
117
- taskObject = await app.serviceViewGet('task', 'task', { task: taskObject.id })
118
- //console.log("UPDATED TASK", taskObject, result)
119
149
  }
120
150
 
121
151
  const runTask = async () => {
@@ -128,19 +158,50 @@ export default function task(definition) {
128
158
  const result = await definition.execute(props, {
129
159
  ...context,
130
160
  task: {
131
- async run(taskFunction, props) {
132
- return await taskFunction(props, {
133
- ...context,
134
- causeType: definition.name + '_Task',
135
- cause: taskObject.id
136
- }, (events) => app.emitEvents(definition.name,
137
- 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
138
187
  },
139
- async progress(current, total) {
140
- await updateTask({
141
- progress: { current, total }
142
- })
188
+ async progress(current, total, action, opts) {
189
+ selfProgress = {
190
+ ...opts,
191
+ current, total, action
192
+ }
193
+ updateProgress()
143
194
  }
195
+ },
196
+ async trigger(trigger, props) {
197
+ return await app.trigger({
198
+ causeType: 'task_Task',
199
+ cause: taskObject.id,
200
+ ...trigger,
201
+ }, props)
202
+ },
203
+ async triggerService(trigger, props, returnArray = false) {
204
+ return await app.triggerService(trigger, props, returnArray)
144
205
  }
145
206
  })
146
207
  await updateTask({
@@ -152,13 +213,19 @@ export default function task(definition) {
152
213
  } catch(error) {
153
214
  console.log("TASK ERROR", error.message, error.stack)
154
215
  /*console.log("RETRIES", taskObject.retries?.length, maxRetries)*/
155
- if((taskObject.retries?.length || 0) >= maxRetries) {
216
+ if((taskObject.retries?.length || 0) >= taskObject.maxRetries) {
156
217
  await updateTask({
157
218
  state: 'failed',
158
219
  doneAt: new Date(),
159
- error: error.stack ?? error.message ?? error
220
+ error: error.stack ?? error.message ?? error,
221
+ retries: [...(taskObject.retries || []), {
222
+ startedAt: taskObject.startedAt,
223
+ failedAt: new Date(),
224
+ error: error.stack ?? error.message ?? error
225
+ }]
160
226
  })
161
227
  } else {
228
+ const retriesCount = (taskObject.retries || []).length
162
229
  await updateTask({
163
230
  state: 'retrying',
164
231
  retries: [...(taskObject.retries || []), {
@@ -167,20 +234,54 @@ export default function task(definition) {
167
234
  error: error.stack ?? error.message ?? error
168
235
  }]
169
236
  })
237
+ await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retriesCount)))
170
238
  }
171
239
  await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
172
240
  }
173
241
  }
174
242
 
175
- /// TODO: implement task queues
176
243
  while(taskObject.state !== 'done' && taskObject.state !== 'failed') {
177
244
  await runTask()
178
- //console.log("TASK OBJECT AFTER RUNTASK", taskObject)
245
+ // console.log("TASK", definition.name, "AFTER RUNTASK", taskObject)
179
246
  }
180
247
 
181
248
  return taskObject.result
182
249
  }
183
250
 
251
+ serviceDefinition.beforeStart(async () => {
252
+ setTimeout(async () => {
253
+ let gt = undefined
254
+ console.log("GT", gt)
255
+ let tasksToRestart = await app.viewGet('runningTaskRootsByName', {
256
+ name: definition.name,
257
+ gt,
258
+ limit: 25
259
+ })
260
+ while(tasksToRestart.length > 0) {
261
+ console.log("FOUND", tasksToRestart.length, "TASKS", definition.name, "TO RESTART")
262
+ for(const task of tasksToRestart) {
263
+ console.log("RESTARTING TASK", task)
264
+ const taskObject = { ...task, id: task.to ?? task.id }
265
+ const context = {
266
+ causeType: task.causeType,
267
+ cause: task.cause,
268
+ taskObject,
269
+ }
270
+ const promise = taskFunction(taskObject.properties, context)
271
+ /// run async = ignore promise
272
+ await new Promise(resolve => setTimeout(resolve, 1000)) // wait a second
273
+ }
274
+ gt = tasksToRestart[tasksToRestart.length - 1].id
275
+ console.log("GT", gt)
276
+ tasksToRestart = await app.viewGet('runningTaskRootsByName', {
277
+ name: definition.name,
278
+ gt,
279
+ limit: 25
280
+ })
281
+ }
282
+ }, 500)
283
+ })
284
+
184
285
  taskFunction.definition = definition
185
286
  taskFunction.start = async (props, causeType, cause) => {
186
287
  return await startTask(taskFunction, props, causeType, cause)