@live-change/task-service 0.8.32 → 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 +100 -0
  2. package/package.json +4 -4
  3. package/task.js +113 -29
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.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.32",
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.32",
26
- "@live-change/relations-plugin": "^0.8.32"
25
+ "@live-change/framework": "^0.8.33",
26
+ "@live-change/relations-plugin": "^0.8.33"
27
27
  },
28
- "gitHead": "9ea7767670a99404794087726223064c45d798d3"
28
+ "gitHead": "98ff6f9c09e5fc1f408010df6cc8038eff571276"
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,18 +158,39 @@ 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
  }
144
195
  }
145
196
  })
@@ -152,13 +203,14 @@ export default function task(definition) {
152
203
  } catch(error) {
153
204
  console.log("TASK ERROR", error.message, error.stack)
154
205
  /*console.log("RETRIES", taskObject.retries?.length, maxRetries)*/
155
- if((taskObject.retries?.length || 0) >= maxRetries) {
206
+ if((taskObject.retries?.length || 0) >= taskObject.maxRetries) {
156
207
  await updateTask({
157
208
  state: 'failed',
158
209
  doneAt: new Date(),
159
210
  error: error.stack ?? error.message ?? error
160
211
  })
161
212
  } else {
213
+ const retriesCount = (taskObject.retries || []).length
162
214
  await updateTask({
163
215
  state: 'retrying',
164
216
  retries: [...(taskObject.retries || []), {
@@ -167,20 +219,52 @@ export default function task(definition) {
167
219
  error: error.stack ?? error.message ?? error
168
220
  }]
169
221
  })
222
+ await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retriesCount)))
170
223
  }
171
224
  await triggerOnTaskStateChange(taskObject, context.causeType, context.cause)
172
225
  }
173
226
  }
174
227
 
175
- /// TODO: implement task queues
176
228
  while(taskObject.state !== 'done' && taskObject.state !== 'failed') {
177
229
  await runTask()
178
- //console.log("TASK OBJECT AFTER RUNTASK", taskObject)
230
+ console.log("TASK", definition.name, "AFTER RUNTASK", taskObject)
179
231
  }
180
232
 
181
233
  return taskObject.result
182
234
  }
183
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
+
184
268
  taskFunction.definition = definition
185
269
  taskFunction.start = async (props, causeType, cause) => {
186
270
  return await startTask(taskFunction, props, causeType, cause)