@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.
- package/model.js +100 -0
- package/package.json +4 -4
- 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.
|
|
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.
|
|
26
|
-
"@live-change/relations-plugin": "^0.8.
|
|
25
|
+
"@live-change/framework": "^0.8.34",
|
|
26
|
+
"@live-change/relations-plugin": "^0.8.34"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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)
|