teraslice 2.11.0 → 2.12.0
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/dist/src/interfaces.js +12 -0
- package/dist/src/lib/cluster/cluster_master.js +246 -0
- package/dist/src/lib/cluster/node_master.js +355 -0
- package/dist/src/lib/cluster/services/api.js +663 -0
- package/dist/src/lib/cluster/services/assets.js +226 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetes/index.js +192 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetes/k8s.js +481 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetes/k8sResource.js +414 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetes/k8sState.js +59 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetes/utils.js +43 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/index.js +192 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/interfaces.js +2 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8s.js +423 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8sDeploymentResource.js +60 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8sJobResource.js +55 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8sResource.js +359 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8sServiceResource.js +37 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/k8sState.js +60 -0
- package/dist/src/lib/cluster/services/cluster/backends/kubernetesV2/utils.js +170 -0
- package/dist/src/lib/cluster/services/cluster/backends/native/dispatch.js +13 -0
- package/dist/src/lib/cluster/services/cluster/backends/native/index.js +526 -0
- package/dist/src/lib/cluster/services/cluster/backends/native/messaging.js +547 -0
- package/dist/src/lib/cluster/services/cluster/backends/state-utils.js +26 -0
- package/dist/src/lib/cluster/services/cluster/index.js +17 -0
- package/dist/src/lib/cluster/services/execution.js +435 -0
- package/dist/src/lib/cluster/services/index.js +6 -0
- package/dist/src/lib/cluster/services/interfaces.js +2 -0
- package/dist/src/lib/cluster/services/jobs.js +454 -0
- package/dist/src/lib/config/default-sysconfig.js +26 -0
- package/dist/src/lib/config/index.js +22 -0
- package/dist/src/lib/config/schemas/system.js +360 -0
- package/dist/src/lib/storage/analytics.js +86 -0
- package/dist/src/lib/storage/assets.js +401 -0
- package/dist/src/lib/storage/backends/elasticsearch_store.js +494 -0
- package/dist/src/lib/storage/backends/mappings/analytics.js +50 -0
- package/dist/src/lib/storage/backends/mappings/asset.js +41 -0
- package/dist/src/lib/storage/backends/mappings/ex.js +62 -0
- package/dist/src/lib/storage/backends/mappings/job.js +38 -0
- package/dist/src/lib/storage/backends/mappings/state.js +38 -0
- package/dist/src/lib/storage/backends/s3_store.js +237 -0
- package/dist/src/lib/storage/execution.js +300 -0
- package/dist/src/lib/storage/index.js +7 -0
- package/dist/src/lib/storage/jobs.js +81 -0
- package/dist/src/lib/storage/state.js +255 -0
- package/dist/src/lib/utils/api_utils.js +157 -0
- package/dist/src/lib/utils/asset_utils.js +94 -0
- package/dist/src/lib/utils/date_utils.js +52 -0
- package/dist/src/lib/utils/encoding_utils.js +27 -0
- package/dist/src/lib/utils/events.js +4 -0
- package/dist/src/lib/utils/file_utils.js +124 -0
- package/dist/src/lib/utils/id_utils.js +15 -0
- package/dist/src/lib/utils/port_utils.js +32 -0
- package/dist/src/lib/workers/assets/index.js +3 -0
- package/dist/src/lib/workers/assets/loader-executable.js +40 -0
- package/dist/src/lib/workers/assets/loader.js +73 -0
- package/dist/src/lib/workers/assets/spawn.js +55 -0
- package/dist/src/lib/workers/context/execution-context.js +12 -0
- package/dist/src/lib/workers/context/terafoundation-context.js +8 -0
- package/dist/src/lib/workers/execution-controller/execution-analytics.js +188 -0
- package/dist/src/lib/workers/execution-controller/index.js +1024 -0
- package/dist/src/lib/workers/execution-controller/recovery.js +151 -0
- package/dist/src/lib/workers/execution-controller/scheduler.js +390 -0
- package/dist/src/lib/workers/execution-controller/slice-analytics.js +96 -0
- package/dist/src/lib/workers/helpers/job.js +80 -0
- package/dist/src/lib/workers/helpers/op-analytics.js +22 -0
- package/dist/src/lib/workers/helpers/terafoundation.js +34 -0
- package/dist/src/lib/workers/helpers/worker-shutdown.js +169 -0
- package/dist/src/lib/workers/metrics/index.js +108 -0
- package/dist/src/lib/workers/worker/index.js +378 -0
- package/dist/src/lib/workers/worker/slice.js +122 -0
- package/dist/test/config/schemas/system_schema-spec.js +37 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/k8s-spec.js +316 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/k8sResource-spec.js +795 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/k8sState-multicluster-spec.js +67 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/k8sState-spec.js +84 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/utils-spec.js +132 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/v2/k8s-v2-spec.js +455 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/v2/k8sResource-v2-spec.js +818 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/v2/k8sState-multicluster-v2-spec.js +67 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/v2/k8sState-v2-spec.js +84 -0
- package/dist/test/lib/cluster/services/cluster/backends/kubernetes/v2/utils-v2-spec.js +320 -0
- package/dist/test/lib/cluster/services/cluster/backends/state-utils-spec.js +37 -0
- package/dist/test/node_master-spec.js +188 -0
- package/dist/test/services/api-spec.js +80 -0
- package/dist/test/services/assets-spec.js +158 -0
- package/dist/test/services/messaging-spec.js +440 -0
- package/dist/test/storage/assets_storage-spec.js +95 -0
- package/dist/test/storage/s3_store-spec.js +138 -0
- package/dist/test/test.config.js +8 -0
- package/dist/test/test.setup.js +6 -0
- package/dist/test/utils/api_utils-spec.js +86 -0
- package/dist/test/utils/asset_utils-spec.js +141 -0
- package/dist/test/utils/elastic_utils-spec.js +25 -0
- package/dist/test/workers/execution-controller/execution-controller-spec.js +371 -0
- package/dist/test/workers/execution-controller/execution-special-test-cases-spec.js +520 -0
- package/dist/test/workers/execution-controller/execution-test-cases-spec.js +338 -0
- package/dist/test/workers/execution-controller/recovery-spec.js +160 -0
- package/dist/test/workers/execution-controller/scheduler-spec.js +249 -0
- package/dist/test/workers/execution-controller/slice-analytics-spec.js +121 -0
- package/dist/test/workers/fixtures/ops/example-op/processor.js +20 -0
- package/dist/test/workers/fixtures/ops/example-op/schema.js +19 -0
- package/dist/test/workers/fixtures/ops/example-reader/fetcher.js +20 -0
- package/dist/test/workers/fixtures/ops/example-reader/schema.js +41 -0
- package/dist/test/workers/fixtures/ops/example-reader/slicer.js +37 -0
- package/dist/test/workers/fixtures/ops/new-op/processor.js +29 -0
- package/dist/test/workers/fixtures/ops/new-op/schema.js +18 -0
- package/dist/test/workers/fixtures/ops/new-reader/fetcher.js +19 -0
- package/dist/test/workers/fixtures/ops/new-reader/schema.js +23 -0
- package/dist/test/workers/fixtures/ops/new-reader/slicer.js +13 -0
- package/dist/test/workers/helpers/configs.js +130 -0
- package/dist/test/workers/helpers/execution-controller-helper.js +49 -0
- package/dist/test/workers/helpers/index.js +5 -0
- package/dist/test/workers/helpers/test-context.js +210 -0
- package/dist/test/workers/helpers/zip-directory.js +25 -0
- package/dist/test/workers/worker/slice-spec.js +333 -0
- package/dist/test/workers/worker/worker-spec.js +356 -0
- package/package.json +94 -94
- package/service.js +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
template: '__state*',
|
|
3
|
+
mappings: {
|
|
4
|
+
state: {
|
|
5
|
+
_all: {
|
|
6
|
+
enabled: false
|
|
7
|
+
},
|
|
8
|
+
dynamic: false,
|
|
9
|
+
properties: {
|
|
10
|
+
ex_id: {
|
|
11
|
+
type: 'keyword'
|
|
12
|
+
},
|
|
13
|
+
slice_id: {
|
|
14
|
+
type: 'keyword'
|
|
15
|
+
},
|
|
16
|
+
slicer_id: {
|
|
17
|
+
type: 'keyword'
|
|
18
|
+
},
|
|
19
|
+
slicer_order: {
|
|
20
|
+
type: 'integer'
|
|
21
|
+
},
|
|
22
|
+
state: {
|
|
23
|
+
type: 'keyword'
|
|
24
|
+
},
|
|
25
|
+
_created: {
|
|
26
|
+
type: 'date'
|
|
27
|
+
},
|
|
28
|
+
_updated: {
|
|
29
|
+
type: 'date'
|
|
30
|
+
},
|
|
31
|
+
error: {
|
|
32
|
+
type: 'keyword'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { TSError, isTest, logError, pDelay, pWhile, random } from '@terascope/utils';
|
|
2
|
+
import { S3ClientResponse, createS3Bucket, deleteS3Object, doesBucketExist, getS3Object, listS3Objects, putS3Object, s3RequestWithRetry } from '@terascope/file-asset-apis';
|
|
3
|
+
import { makeLogger } from '../../workers/helpers/terafoundation.js';
|
|
4
|
+
export class S3Store {
|
|
5
|
+
bucket;
|
|
6
|
+
connection;
|
|
7
|
+
terafoundation;
|
|
8
|
+
isShuttingDown;
|
|
9
|
+
logger;
|
|
10
|
+
api;
|
|
11
|
+
context;
|
|
12
|
+
constructor(backendConfig) {
|
|
13
|
+
const { context, terafoundation, connection, bucket, logger } = backendConfig;
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.bucket = bucket || this.createDefaultBucketName();
|
|
16
|
+
this.connection = connection;
|
|
17
|
+
this.isShuttingDown = false;
|
|
18
|
+
this.logger = logger ?? makeLogger(context, 's3_backend', { storageName: this.bucket });
|
|
19
|
+
this.terafoundation = terafoundation;
|
|
20
|
+
}
|
|
21
|
+
async initialize() {
|
|
22
|
+
const { client } = await this.context.apis.foundation
|
|
23
|
+
.createClient({ type: 's3', cached: true, endpoint: this.connection });
|
|
24
|
+
this.api = client;
|
|
25
|
+
await pWhile(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const exists = await doesBucketExist(client, { Bucket: this.bucket });
|
|
28
|
+
if (!exists) {
|
|
29
|
+
await createS3Bucket(client, { Bucket: this.bucket });
|
|
30
|
+
}
|
|
31
|
+
const isReady = await this.verifyClient();
|
|
32
|
+
return isReady;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.Code === 'InvalidAccessKeyId') {
|
|
36
|
+
throw new TSError(`accessKeyId ${this.terafoundation.connectors.s3[this.connection].accessKeyId} specified in S3 ${this.connection} does not exit: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
if (err.Code === 'SignatureDoesNotMatch') {
|
|
39
|
+
throw new TSError(`secretAccessKey specified in S3 ${this.connection} does not match accessKeyId: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
if (err.Code === 'InvalidBucketName') {
|
|
42
|
+
throw new TSError(`Bucket name does not follow S3 naming rules: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (err instanceof S3ClientResponse.BucketAlreadyExists) {
|
|
45
|
+
throw new TSError(`Bucket name ${this.bucket} not available. accessKeyId ${this.terafoundation.connectors.s3[this.connection].accessKeyId} does not own this bucket. ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
logError(this.logger, err, `Failed attempt connecting to S3 ${this.connection} connection, ${this.bucket} bucket (will retry)`);
|
|
48
|
+
await pDelay(isTest ? 0 : random(2000, 4000));
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// TODO: if we want to use the S3 store more generically we can't
|
|
54
|
+
// assume the key will have a '.zip' extension
|
|
55
|
+
async get(recordId) {
|
|
56
|
+
const command = {
|
|
57
|
+
Bucket: this.bucket,
|
|
58
|
+
Key: `${recordId}.zip`
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
this.logger.debug(`getting record with id: ${recordId} from s3 ${this.connection} connection, ${this.bucket} bucket.`);
|
|
62
|
+
const client = this.api;
|
|
63
|
+
const bufferArray = [];
|
|
64
|
+
let triggerReturn = false;
|
|
65
|
+
const response = await s3RequestWithRetry({
|
|
66
|
+
client,
|
|
67
|
+
func: getS3Object,
|
|
68
|
+
params: command
|
|
69
|
+
});
|
|
70
|
+
/// Convert the response body to a Node read stream
|
|
71
|
+
const s3Stream = response.Body;
|
|
72
|
+
/// Store the data coming into s3 into a buffer array
|
|
73
|
+
s3Stream.on('data', (chunk) => {
|
|
74
|
+
bufferArray.push(chunk);
|
|
75
|
+
});
|
|
76
|
+
s3Stream.on('end', () => {
|
|
77
|
+
triggerReturn = true;
|
|
78
|
+
});
|
|
79
|
+
s3Stream.on('error', (err) => {
|
|
80
|
+
throw new TSError(`Unable to get recordId ${recordId} from s3 ${this.connection} connection, ${this.bucket} bucket.
|
|
81
|
+
Reason: ${err.message}`);
|
|
82
|
+
});
|
|
83
|
+
await pWhile(async () => triggerReturn);
|
|
84
|
+
return Buffer.concat(bufferArray);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err instanceof S3ClientResponse.NoSuchKey) {
|
|
88
|
+
throw new TSError(`recordId ${recordId} does not exist in s3 ${this.connection} connection, ${this.bucket} bucket.`, {
|
|
89
|
+
statusCode: 404
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw new TSError(`Retrieval of recordId ${recordId} from s3 ${this.connection} connection, ${this.bucket} bucket failed: `, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async save(recordId, data, timeout) {
|
|
96
|
+
try {
|
|
97
|
+
this.logger.debug(`saving recordId ${recordId} to s3 ${this.connection} connection, ${this.bucket} bucket.`);
|
|
98
|
+
const command = {
|
|
99
|
+
Bucket: this.bucket,
|
|
100
|
+
Key: `${recordId}.zip`,
|
|
101
|
+
Body: data
|
|
102
|
+
};
|
|
103
|
+
const timeoutID = setTimeout(() => {
|
|
104
|
+
throw new TSError(`Timeout saving recordId ${recordId}`);
|
|
105
|
+
}, timeout);
|
|
106
|
+
try {
|
|
107
|
+
const client = this.api;
|
|
108
|
+
await s3RequestWithRetry({
|
|
109
|
+
client,
|
|
110
|
+
func: putS3Object,
|
|
111
|
+
params: command
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new TSError(`Failure saving recordId ${recordId} to S3: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeoutID);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
throw new TSError(`Error saving recordId ${recordId} to S3 ${this.connection} connection, ${this.bucket} bucket: ${err}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async remove(recordId) {
|
|
126
|
+
try {
|
|
127
|
+
this.logger.debug(`removing record ${recordId} from s3 ${this.connection} connection, ${this.bucket} bucket.`);
|
|
128
|
+
const command = {
|
|
129
|
+
Bucket: this.bucket,
|
|
130
|
+
Key: `${recordId}.zip`
|
|
131
|
+
};
|
|
132
|
+
const client = this.api;
|
|
133
|
+
await s3RequestWithRetry({
|
|
134
|
+
client,
|
|
135
|
+
func: deleteS3Object,
|
|
136
|
+
params: command
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new TSError(`Error deleting recordId ${recordId} from s3 ${this.connection} connection, ${this.bucket} bucket: ${err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async list() {
|
|
144
|
+
/// list all keys in bucket
|
|
145
|
+
const objectList = [];
|
|
146
|
+
let nextContinuationToken;
|
|
147
|
+
let continuePagination = true;
|
|
148
|
+
try {
|
|
149
|
+
do {
|
|
150
|
+
const command = {
|
|
151
|
+
Bucket: this.bucket,
|
|
152
|
+
ContinuationToken: nextContinuationToken || undefined,
|
|
153
|
+
// MaxKeys: 1000 // Default is 1000
|
|
154
|
+
};
|
|
155
|
+
const client = this.api;
|
|
156
|
+
const response = await s3RequestWithRetry({
|
|
157
|
+
client,
|
|
158
|
+
func: listS3Objects,
|
|
159
|
+
params: command
|
|
160
|
+
});
|
|
161
|
+
response.Contents?.forEach((c) => {
|
|
162
|
+
const s3Record = {
|
|
163
|
+
File: c.Key,
|
|
164
|
+
Size: c.Size,
|
|
165
|
+
// Created: c.LastModified
|
|
166
|
+
};
|
|
167
|
+
objectList.push(s3Record);
|
|
168
|
+
});
|
|
169
|
+
if (!response.IsTruncated) {
|
|
170
|
+
continuePagination = false;
|
|
171
|
+
nextContinuationToken = undefined;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
nextContinuationToken = response.NextContinuationToken;
|
|
175
|
+
}
|
|
176
|
+
const recordsReceived = response.Contents?.length || 0;
|
|
177
|
+
this.logger.debug(`Received ${recordsReceived} records from s3 ${this.connection} connection, ${this.bucket} bucket.`);
|
|
178
|
+
} while (continuePagination);
|
|
179
|
+
this.logger.info(`Found ${objectList.length} records in s3 ${this.connection} connection, ${this.bucket} bucket.`);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
throw new TSError(`Error listing records from s3 ${this.connection} connection, ${this.bucket} bucket: ${err}`);
|
|
183
|
+
}
|
|
184
|
+
return objectList;
|
|
185
|
+
}
|
|
186
|
+
/*
|
|
187
|
+
* The S3 client has no built in functionality to determine if the client is connected.
|
|
188
|
+
* If we can make a request to ListS3Objects then we know that the bucket exists and
|
|
189
|
+
* credentials are valid.
|
|
190
|
+
*/
|
|
191
|
+
async verifyClient() {
|
|
192
|
+
if (this.isShuttingDown)
|
|
193
|
+
return false;
|
|
194
|
+
const command = {
|
|
195
|
+
Bucket: this.bucket
|
|
196
|
+
};
|
|
197
|
+
const config = this.terafoundation.connectors.s3[this.connection];
|
|
198
|
+
try {
|
|
199
|
+
const client = this.api;
|
|
200
|
+
await s3RequestWithRetry({
|
|
201
|
+
client,
|
|
202
|
+
func: listS3Objects,
|
|
203
|
+
params: command
|
|
204
|
+
});
|
|
205
|
+
this.logger.debug(`S3 Client verification succeeded. Connection: ${this.connection}, endpoint: ${config.endpoint}`);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
this.logger.debug(`S3 Client verification failed. Connection: ${this.connection}, endpoint: ${config.endpoint}: `, err);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async waitForClient() {
|
|
214
|
+
this.logger.debug('waiting for s3 client');
|
|
215
|
+
if (await this.verifyClient())
|
|
216
|
+
return;
|
|
217
|
+
await pWhile(async () => {
|
|
218
|
+
if (this.isShuttingDown)
|
|
219
|
+
throw new Error('S3 store is shutdown');
|
|
220
|
+
if (await this.verifyClient())
|
|
221
|
+
return true;
|
|
222
|
+
await pDelay(100);
|
|
223
|
+
return false;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async shutdown() {
|
|
227
|
+
this.isShuttingDown = true;
|
|
228
|
+
this.api.destroy();
|
|
229
|
+
}
|
|
230
|
+
// TODO: Use more generic bucket prefix (not related to assets)
|
|
231
|
+
// or pass in prefix from calling class
|
|
232
|
+
createDefaultBucketName() {
|
|
233
|
+
const safeName = this.context.sysconfig.teraslice.name.replaceAll('_', '-');
|
|
234
|
+
return `ts-assets-${safeName}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=s3_store.js.map
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { TSError, includes, getTypeOf, makeISODate } from '@terascope/utils';
|
|
2
|
+
import { RecoveryCleanupType } from '@terascope/job-components';
|
|
3
|
+
import { v4 as uuid } from 'uuid';
|
|
4
|
+
import { makeLogger } from '../workers/helpers/terafoundation.js';
|
|
5
|
+
import { TerasliceElasticsearchStorage } from './backends/elasticsearch_store.js';
|
|
6
|
+
const INIT_STATUS = ['pending', 'scheduling', 'initializing'];
|
|
7
|
+
const RUNNING_STATUS = ['recovering', 'running', 'failing', 'paused', 'stopping'];
|
|
8
|
+
const TERMINAL_STATUS = ['completed', 'stopped', 'rejected', 'failed', 'terminated'];
|
|
9
|
+
const VALID_STATUS = INIT_STATUS.concat(RUNNING_STATUS).concat(TERMINAL_STATUS);
|
|
10
|
+
// Module to manager job states in Elasticsearch.
|
|
11
|
+
// All functions in this module return promises that must be resolved to
|
|
12
|
+
// get the final result.
|
|
13
|
+
export class ExecutionStorage {
|
|
14
|
+
backend;
|
|
15
|
+
jobType;
|
|
16
|
+
logger;
|
|
17
|
+
constructor(context) {
|
|
18
|
+
const logger = makeLogger(context, 'ex_storage');
|
|
19
|
+
const config = context.sysconfig.teraslice;
|
|
20
|
+
const jobType = 'ex';
|
|
21
|
+
const indexName = `${config.name}__ex`;
|
|
22
|
+
const backendConfig = {
|
|
23
|
+
context,
|
|
24
|
+
indexName,
|
|
25
|
+
recordType: 'ex',
|
|
26
|
+
idField: 'ex_id',
|
|
27
|
+
fullResponse: false,
|
|
28
|
+
logRecord: false,
|
|
29
|
+
storageName: 'execution',
|
|
30
|
+
logger
|
|
31
|
+
};
|
|
32
|
+
this.jobType = jobType;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
this.backend = new TerasliceElasticsearchStorage(backendConfig);
|
|
35
|
+
if (context.apis.executionContext) {
|
|
36
|
+
context.apis.executionContext.registerMetadataFns({ get: this.getMetadata.bind(this), update: this.updateMetadata.bind(this) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async initialize() {
|
|
40
|
+
await this.backend.initialize();
|
|
41
|
+
this.logger.info('execution storage initialized');
|
|
42
|
+
}
|
|
43
|
+
async get(exId) {
|
|
44
|
+
const results = await this.backend.get(exId);
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
// encompasses all executions in either initialization or running statuses
|
|
48
|
+
async getActiveExecution(exId) {
|
|
49
|
+
const str = this.getTerminalStatuses().map((state) => ` _status:${state} `)
|
|
50
|
+
.join('OR');
|
|
51
|
+
const query = `ex_id:"${exId}" NOT (${str.trim()})`;
|
|
52
|
+
const executions = await this.backend.search(query, undefined, 1, '_created:desc');
|
|
53
|
+
if (!executions.length) {
|
|
54
|
+
throw new TSError(`no active execution context was found for ex_id: ${exId}`, {
|
|
55
|
+
statusCode: 404
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return executions[0];
|
|
59
|
+
}
|
|
60
|
+
async search(query, from, size, sort, fields) {
|
|
61
|
+
const results = await this.backend.search(query, from, size, sort, fields);
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
async create(record, status = 'pending') {
|
|
65
|
+
if (!this._isValidStatus(status)) {
|
|
66
|
+
throw new Error(`Unknown status "${status}" on execution create`);
|
|
67
|
+
}
|
|
68
|
+
if (!record.job_id) {
|
|
69
|
+
throw new Error('Missing job_id on execution create');
|
|
70
|
+
}
|
|
71
|
+
const date = makeISODate();
|
|
72
|
+
const doc = Object.assign({}, record, {
|
|
73
|
+
ex_id: uuid(),
|
|
74
|
+
metadata: {},
|
|
75
|
+
_status: status,
|
|
76
|
+
_context: this.jobType,
|
|
77
|
+
_created: date,
|
|
78
|
+
_updated: date,
|
|
79
|
+
_has_errors: false,
|
|
80
|
+
_slicer_stats: {},
|
|
81
|
+
_failureReason: ''
|
|
82
|
+
});
|
|
83
|
+
// @ts-expect-error
|
|
84
|
+
delete doc.slicer_port;
|
|
85
|
+
// @ts-expect-error
|
|
86
|
+
delete doc.slicer_hostname;
|
|
87
|
+
try {
|
|
88
|
+
await this.backend.create(doc);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new TSError(err, {
|
|
92
|
+
reason: 'Failure to create execution context'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return doc;
|
|
96
|
+
}
|
|
97
|
+
async updatePartial(exId, applyChanges) {
|
|
98
|
+
return this.backend.updatePartial(exId, applyChanges);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* @typedef ExErrorMetadata
|
|
102
|
+
* @property _has_errors {boolean}
|
|
103
|
+
* @property _failureReason {string}
|
|
104
|
+
* @property _slicer_stats {import(
|
|
105
|
+
* '../workers/execution-controller/execution-analytics.js'
|
|
106
|
+
* ).ExecutionStats}
|
|
107
|
+
*/
|
|
108
|
+
/**
|
|
109
|
+
* Format the execution error stats, primarly used for updating the
|
|
110
|
+
* status.
|
|
111
|
+
*
|
|
112
|
+
* If no error message is passed, it will reset the _has_errors and _failureReason.
|
|
113
|
+
* If execution stats is provided it will set the _slicer_stats
|
|
114
|
+
*
|
|
115
|
+
* @param stats {import(
|
|
116
|
+
* '../workers/execution-controller/execution-analytics.js'
|
|
117
|
+
* ).ExecutionStats=}
|
|
118
|
+
* @param errMsg {string=}
|
|
119
|
+
* @return {ExErrorMetadata}
|
|
120
|
+
*/
|
|
121
|
+
// TODO: type out stats
|
|
122
|
+
executionMetaData(stats, errMsg) {
|
|
123
|
+
const errMetadata = {
|
|
124
|
+
_has_errors: false,
|
|
125
|
+
_failureReason: ''
|
|
126
|
+
};
|
|
127
|
+
const statsMetadata = {};
|
|
128
|
+
if (errMsg) {
|
|
129
|
+
errMetadata._has_errors = true;
|
|
130
|
+
errMetadata._failureReason = errMsg;
|
|
131
|
+
}
|
|
132
|
+
if (stats) {
|
|
133
|
+
statsMetadata._slicer_stats = Object.assign({}, stats);
|
|
134
|
+
}
|
|
135
|
+
return Object.assign({}, errMetadata, statsMetadata);
|
|
136
|
+
}
|
|
137
|
+
async getMetadata(exId) {
|
|
138
|
+
const ex = await this.get(exId);
|
|
139
|
+
return ex.metadata ?? {};
|
|
140
|
+
}
|
|
141
|
+
// TODO: type this
|
|
142
|
+
async updateMetadata(exId, metadata = {}) {
|
|
143
|
+
await this.backend.update(exId, {
|
|
144
|
+
metadata,
|
|
145
|
+
_updated: makeISODate()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// TODO: put a type of return
|
|
149
|
+
async getStatus(exId) {
|
|
150
|
+
try {
|
|
151
|
+
const result = await this.get(exId);
|
|
152
|
+
return result._status;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
throw new TSError(err, {
|
|
156
|
+
reason: `Cannot get execution status ${exId}`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// TODO: type this
|
|
161
|
+
// verify the current status to make sure it can be updated to the desired status
|
|
162
|
+
async verifyStatusUpdate(exId, desiredStatus) {
|
|
163
|
+
if (!desiredStatus || !this._isValidStatus(desiredStatus)) {
|
|
164
|
+
throw new TSError(`Invalid Job status: "${desiredStatus}"`, {
|
|
165
|
+
statusCode: 422
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const status = await this.getStatus(exId);
|
|
169
|
+
this._verifyStatus(status, desiredStatus);
|
|
170
|
+
}
|
|
171
|
+
_verifyStatus(status, desiredStatus) {
|
|
172
|
+
// when setting the same status to shouldn't throw an error
|
|
173
|
+
if (desiredStatus === status) {
|
|
174
|
+
return status;
|
|
175
|
+
}
|
|
176
|
+
// when the current status is running it cannot be set to an init status
|
|
177
|
+
if (this._isRunningStatus(status) && this._isInitStatus(desiredStatus)) {
|
|
178
|
+
throw new TSError(`Cannot update running job status of "${status}" to init status of "${desiredStatus}"`, {
|
|
179
|
+
statusCode: 422
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// if it is set to stop but the execution finishes before it can stop
|
|
183
|
+
// it is okay to set it to completed
|
|
184
|
+
if (status === 'stopped' && desiredStatus === 'completed') {
|
|
185
|
+
return status;
|
|
186
|
+
}
|
|
187
|
+
// when the status is a terminal status, it cannot be set to again
|
|
188
|
+
if (this._isTerminalStatus(status)) {
|
|
189
|
+
throw new TSError(`Cannot update terminal job status of "${status}" to "${desiredStatus}"`, {
|
|
190
|
+
statusCode: 422
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// otherwise allow the update
|
|
194
|
+
return status;
|
|
195
|
+
}
|
|
196
|
+
// TODO: type this
|
|
197
|
+
/**
|
|
198
|
+
* Set the status
|
|
199
|
+
*
|
|
200
|
+
* @param {string} exId
|
|
201
|
+
* @param {string} status
|
|
202
|
+
* @param {Partial<import('@terascope/job-components').ExecutionConfig>} body
|
|
203
|
+
* @returns {Promise<import('@terascope/job-components').ExecutionConfig>}
|
|
204
|
+
*/
|
|
205
|
+
async setStatus(exId, status, body) {
|
|
206
|
+
try {
|
|
207
|
+
return await this.updatePartial(exId, async (existing) => {
|
|
208
|
+
this._verifyStatus(existing._status, status);
|
|
209
|
+
return Object.assign(existing, body, {
|
|
210
|
+
_status: status,
|
|
211
|
+
_updated: makeISODate()
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
throw new TSError(err, {
|
|
217
|
+
statusCode: 422,
|
|
218
|
+
reason: `Unable to set execution ${exId} status code to ${status}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async softDelete(exId) {
|
|
223
|
+
try {
|
|
224
|
+
const date = makeISODate();
|
|
225
|
+
return await this.updatePartial(exId, async (existing) => Object.assign(existing, {
|
|
226
|
+
_deleted: true,
|
|
227
|
+
_deleted_on: date,
|
|
228
|
+
_updated: date
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
throw new TSError(err, {
|
|
233
|
+
statusCode: 422,
|
|
234
|
+
reason: `Unable to delete execution ${exId}`
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async remove(exId) {
|
|
239
|
+
return this.backend.remove(exId);
|
|
240
|
+
}
|
|
241
|
+
async shutdown(forceShutdown) {
|
|
242
|
+
this.logger.info('shutting down.');
|
|
243
|
+
return this.backend.shutdown(forceShutdown);
|
|
244
|
+
}
|
|
245
|
+
verifyClient() {
|
|
246
|
+
return this.backend.verifyClient();
|
|
247
|
+
}
|
|
248
|
+
async waitForClient() {
|
|
249
|
+
return this.backend.waitForClient();
|
|
250
|
+
}
|
|
251
|
+
getTerminalStatuses() {
|
|
252
|
+
return TERMINAL_STATUS.slice();
|
|
253
|
+
}
|
|
254
|
+
getRunningStatuses() {
|
|
255
|
+
return RUNNING_STATUS.slice();
|
|
256
|
+
}
|
|
257
|
+
getLivingStatuses() {
|
|
258
|
+
return INIT_STATUS.concat(RUNNING_STATUS);
|
|
259
|
+
}
|
|
260
|
+
_isValidStatus(status) {
|
|
261
|
+
return includes(VALID_STATUS, status);
|
|
262
|
+
}
|
|
263
|
+
_isRunningStatus(status) {
|
|
264
|
+
return includes(RUNNING_STATUS, status);
|
|
265
|
+
}
|
|
266
|
+
_isTerminalStatus(status) {
|
|
267
|
+
return includes(TERMINAL_STATUS, status);
|
|
268
|
+
}
|
|
269
|
+
_isInitStatus(status) {
|
|
270
|
+
return includes(INIT_STATUS, status);
|
|
271
|
+
}
|
|
272
|
+
// TODO: fix types
|
|
273
|
+
/**
|
|
274
|
+
* @param {import('@terascope/job-components').ExecutionConfig} recoverFrom
|
|
275
|
+
* @param {RecoveryCleanupType} [cleanupType]
|
|
276
|
+
* @returns {Promise<import('@terascope/job-components').ExecutionConfig>}
|
|
277
|
+
*/
|
|
278
|
+
async createRecoveredExecution(recoverFrom, cleanupType) {
|
|
279
|
+
if (!recoverFrom) {
|
|
280
|
+
throw new Error(`Invalid execution given, got ${getTypeOf(recoverFrom)}`);
|
|
281
|
+
}
|
|
282
|
+
if (!recoverFrom.ex_id) {
|
|
283
|
+
throw new Error('Unable to recover execution with missing ex_id');
|
|
284
|
+
}
|
|
285
|
+
const recoverFromId = recoverFrom.ex_id;
|
|
286
|
+
const ex = Object.assign({}, recoverFrom);
|
|
287
|
+
if (cleanupType && !RecoveryCleanupType[cleanupType]) {
|
|
288
|
+
throw new Error(`Unknown cleanup type "${cleanupType}" to recover`);
|
|
289
|
+
}
|
|
290
|
+
ex.recovered_execution = recoverFromId;
|
|
291
|
+
if (cleanupType) {
|
|
292
|
+
ex.recovered_slice_type = cleanupType;
|
|
293
|
+
}
|
|
294
|
+
else if (ex.autorecover) {
|
|
295
|
+
ex.recovered_slice_type = RecoveryCleanupType.pending;
|
|
296
|
+
}
|
|
297
|
+
return this.create(ex);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=execution.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AnalyticsStorage } from './analytics.js';
|
|
2
|
+
import { AssetsStorage } from './assets.js';
|
|
3
|
+
import { ExecutionStorage } from './execution.js';
|
|
4
|
+
import { JobsStorage } from './jobs.js';
|
|
5
|
+
import { StateStorage, SliceState } from './state.js';
|
|
6
|
+
export { SliceState, AnalyticsStorage, AssetsStorage, ExecutionStorage, JobsStorage, StateStorage, };
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
import { TSError, makeISODate } from '@terascope/utils';
|
|
3
|
+
import { makeLogger } from '../workers/helpers/terafoundation.js';
|
|
4
|
+
import { TerasliceElasticsearchStorage } from './backends/elasticsearch_store.js';
|
|
5
|
+
export class JobsStorage {
|
|
6
|
+
backend;
|
|
7
|
+
jobType;
|
|
8
|
+
logger;
|
|
9
|
+
constructor(context) {
|
|
10
|
+
const logger = makeLogger(context, 'job_storage');
|
|
11
|
+
const config = context.sysconfig.teraslice;
|
|
12
|
+
const jobType = 'job';
|
|
13
|
+
const indexName = `${config.name}__jobs`;
|
|
14
|
+
const backendConfig = {
|
|
15
|
+
context,
|
|
16
|
+
indexName,
|
|
17
|
+
recordType: 'job',
|
|
18
|
+
idField: 'job_id',
|
|
19
|
+
fullResponse: false,
|
|
20
|
+
logRecord: false,
|
|
21
|
+
storageName: 'jobs',
|
|
22
|
+
logger
|
|
23
|
+
};
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
this.jobType = jobType;
|
|
26
|
+
this.backend = new TerasliceElasticsearchStorage(backendConfig);
|
|
27
|
+
}
|
|
28
|
+
async initialize() {
|
|
29
|
+
await this.backend.initialize();
|
|
30
|
+
this.logger.info('job storage initialized');
|
|
31
|
+
}
|
|
32
|
+
async get(jobId) {
|
|
33
|
+
const doc = await this.backend.get(jobId);
|
|
34
|
+
return doc;
|
|
35
|
+
}
|
|
36
|
+
async search(query, from, size, sort, fields) {
|
|
37
|
+
const results = await this.backend.search(query, from, size, sort, fields);
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
async create(record) {
|
|
41
|
+
const date = makeISODate();
|
|
42
|
+
const doc = Object.assign({}, record, {
|
|
43
|
+
job_id: uuid(),
|
|
44
|
+
_context: this.jobType,
|
|
45
|
+
_created: date,
|
|
46
|
+
_updated: date
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
await this.backend.create(doc);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new TSError(err, {
|
|
53
|
+
reason: 'Failure to create job'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return doc;
|
|
57
|
+
}
|
|
58
|
+
async update(jobId, updateSpec) {
|
|
59
|
+
// We want to save the whole job as it is posted, update api does partial doc updates
|
|
60
|
+
const results = await this.backend.indexWithId(jobId, Object.assign({}, updateSpec, {
|
|
61
|
+
job_id: jobId,
|
|
62
|
+
_context: this.jobType,
|
|
63
|
+
_updated: makeISODate()
|
|
64
|
+
}));
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
async remove(jobId) {
|
|
68
|
+
return this.backend.remove(jobId);
|
|
69
|
+
}
|
|
70
|
+
async shutdown(forceShutdown) {
|
|
71
|
+
this.logger.info('shutting down.');
|
|
72
|
+
return this.backend.shutdown(forceShutdown);
|
|
73
|
+
}
|
|
74
|
+
verifyClient() {
|
|
75
|
+
return this.backend.verifyClient();
|
|
76
|
+
}
|
|
77
|
+
async waitForClient() {
|
|
78
|
+
return this.backend.waitForClient();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=jobs.js.map
|