@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.
- package/model.js +101 -1
- package/package.json +4 -4
- 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.
|
|
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,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
|
|
9
|
+
return string[0].toUpperCase() + string.slice(1)
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
async function triggerOnTaskStateChange(taskObject, causeType, cause) {
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
102
|
+
const updateQueue = new PQueue({ concurrency: 1 })
|
|
95
103
|
|
|
96
104
|
async function updateTask(data) {
|
|
97
|
-
|
|
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: '
|
|
110
|
+
type: 'task_updateCauseOwnedTask',
|
|
100
111
|
causeType: context.causeType,
|
|
101
112
|
cause: context.cause,
|
|
102
113
|
task: taskObject.id
|
|
103
114
|
})
|
|
104
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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)
|