@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.
- package/model.js +100 -0
- package/package.json +4 -4
- 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.
|
|
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.
|
|
26
|
-
"@live-change/relations-plugin": "^0.8.
|
|
25
|
+
"@live-change/framework": "^0.8.33",
|
|
26
|
+
"@live-change/relations-plugin": "^0.8.33"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
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
|
-
|
|
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,18 +158,39 @@ 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
|
}
|
|
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
|
-
|
|
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)
|