UNPKG

@apilogic/migration-tool-api

Version:

Universal sql/no-sql database serverless migration tool

490 lines 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpensearchClient = void 0; const opensearch_1 = require("@opensearch-project/opensearch"); const fs = require("fs"); class OpensearchClient { constructor(config) { this._client = OpensearchClient.initClient(config); } getClient() { return this._client; } static initClient(config) { const options = this.getOptions(config.config); console.log(options); return new opensearch_1.Client(options); } static getOptions(options) { const devMode = options.devmode; const prefix = options.envPrefix || 'AWS'; const region = options.region || process.env[`${prefix}_REGION`]; const host = options.url.host || process.env[`${prefix}_HOST`]; delete options.region; if (!region) { throw new TypeError('region is required'); } const config = Object.assign({}, options, { node: host, auth: { username: options.url.username, password: options.url.password, }, }); return config; } load(payload) { this._payload = payload; return Promise.resolve(undefined); } async createIndex(index, workingDir, settingsPath, mappingsPath, alias, modelName) { if (!alias) { throw new Error(`Alias must be provided!`); } const indexExist = await this._client.indices.exists({ index }); console.log('EXISTS ' + indexExist); if (!indexExist.body) { const settings = JSON.parse(fs.readFileSync(workingDir + settingsPath, 'utf8')); console.log('Create index params: ', settings); await this._client.indices.create({ index, body: { settings, aliases: { [alias]: {} } }, }); } const properties = JSON.parse(fs.readFileSync(workingDir + mappingsPath, 'utf-8')); let modelId; if (this._mappingsNeedModelId(properties)) { if (!modelName) { throw new Error(`Mappings contain semantic fields without "model_id", but no modelName was provided for index "${index}".`); } modelId = await this._resolveDeployedModelIdByName(modelName); } return this._mappingsPayload(index, workingDir, mappingsPath, modelId); } async upsertIndexAlias(aliasName, newIndexName, oldIndexName) { console.log(`Moving alias ${aliasName} from index ${oldIndexName} to index ${newIndexName}`); if (oldIndexName) { try { const requestParams = { index: oldIndexName, name: aliasName }; console.log(`Delete alias ${aliasName} from index ${oldIndexName}`); await this._client.indices.deleteAlias(requestParams); } catch (error) { console.warn(error); } } try { const requestParams = { index: newIndexName, name: aliasName }; console.log(`Adding alias ${aliasName} to index ${newIndexName}`); return this._client.indices.putAlias(requestParams); } catch (error) { console.error(error); } } async updateIndex(index, workingDir, mappingsPath, modelName) { const properties = JSON.parse(fs.readFileSync(workingDir + mappingsPath, 'utf-8')); let modelId; if (this._mappingsNeedModelId(properties)) { if (!modelName) { throw new Error(`Mappings contain semantic fields without "model_id", but no modelName was provided for index "${index}".`); } modelId = await this._resolveDeployedModelIdByName(modelName); } return this._mappingsPayload(index, workingDir, mappingsPath, modelId); } async deleteIndex(index) { return this._client.indices.delete({ index }); } async putScript(id, script) { if (!id || typeof id !== 'string') { throw new Error('putScript: "id" is required'); } if (!script || !script.source || typeof script.source !== 'string') { throw new Error('putScript: "source" is required'); } const lang = (script.lang && script.lang.trim()) || 'painless'; const body = { script: { lang, source: script.source } }; if (script.params && Object.keys(script.params).length > 0) { body.script.params = script.params; } try { return this._client.putScript({ id, body }); } catch (e) { const reason = e?.meta?.body?.error?.reason || e?.message || e; throw new Error(`Error inserting script "${id}": ${reason}`); } } async deleteScript(id) { try { return this._client.deleteScript({ id }); } catch (e) { throw new Error(`Error deleting script with id: ${id}`); } } async reindexData(sourceIndex, destinationIndex) { const destinationIndexResponse = await this._client.indices.exists({ index: destinationIndex }); const sourceIndexResponse = await this._client.indices.exists({ index: sourceIndex }); if (destinationIndexResponse.statusCode === 404) { throw new Error(`Index does not exist. Cannot reindex data from ${sourceIndex} to non-existent ${destinationIndex}`); } if (sourceIndexResponse.statusCode === 404) { throw new Error(`Source index ${sourceIndex} provided for reindex operation does not exist`); } try { await this._client.reindex({ requests_per_second: 100, body: { source: { index: sourceIndex }, dest: { index: destinationIndex }, }, }); } catch (e) { throw new Error(`Error occured during reindex operation: Source index: ${sourceIndex}, Destination index: ${destinationIndex} with error: ${e}`); } const sourceIndexAliasesResponse = await this._client.cat.aliases({ v: true, format: 'json' }); const aliases = sourceIndexAliasesResponse.body .filter((row) => row['index'] === sourceIndex) .map((row) => ({ name: row['alias'] })); try { for (const alias of aliases) { await this.upsertIndexAlias(alias.name, destinationIndex, sourceIndex); } return { statusCode: 200, body: aliases }; } catch (e) { throw new Error('Error during upserting aliases'); } } async migratePretrainedModel(params) { const cleanModelName = (params.modelName ?? '').trim(); const cleanGroupName = ((params.groupName ?? '') || cleanModelName).trim(); const cleanGroupDesc = (params.groupDescription ?? '').trim(); if (!cleanModelName) throw new Error('migratePretrainedModel: "modelName" is required.'); if (!cleanGroupName) throw new Error('migratePretrainedModel: "groupName" is required (after fallback).'); const { version, modelFormat, waitForModelIdMs = 120000, pollIntervalMs = 3000, deployTimeoutMs = 180000, deployPollIntervalMs = 3000, } = params; const model_group_id = await this._ensureModelGroup(cleanGroupName, cleanGroupDesc); const primary = await this._ensureModelByNameAndFormat({ name: cleanModelName, version, model_group_id, model_format: modelFormat, waitForModelIdMs, pollIntervalMs, }); let model_id = primary; try { const state = await this._getModelState(model_id); if (!this._isDeployedState(state)) { const taskId = await this._deployModel(model_id); if (taskId) { await this._waitForDeployment(taskId, deployTimeoutMs, deployPollIntervalMs); } await this._waitUntilDeployed(model_id, deployTimeoutMs, deployPollIntervalMs); } return { model_group_id, model_id }; } catch (e) { const msg = (e?.message || '') + ' ' + JSON.stringify(e?.reason || e?.meta?.body?.error || ''); const unsupportedTorch = /Unknown builtin op: aten::/i.test(msg) || /scaled_dot_product_attention/i.test(msg); if (modelFormat === 'TORCH_SCRIPT' && unsupportedTorch) { const onnxId = await this._ensureModelByNameAndFormat({ name: cleanModelName, version, model_group_id, model_format: 'ONNX', waitForModelIdMs, pollIntervalMs, }); const taskId = await this._deployModel(onnxId); if (taskId) { await this._waitForDeployment(taskId, deployTimeoutMs, deployPollIntervalMs); } await this._waitUntilDeployed(onnxId, deployTimeoutMs, deployPollIntervalMs); return { model_group_id, model_id: onnxId }; } throw e; } } async _ensureModelByNameAndFormat(args) { const { name, version, model_group_id, model_format, waitForModelIdMs, pollIntervalMs } = args; let model_id = await this._searchModelIdByName(name, model_format); if (!model_id) { const reg = await this._registerPretrainedModel({ name, version, model_group_id, model_format, }); model_id = reg.model_id ?? null; if (!model_id && reg.task_id) { model_id = await this._waitForModelRegistration(reg.task_id, waitForModelIdMs, pollIntervalMs); } if (!model_id) { const started = Date.now(); while (Date.now() - started < waitForModelIdMs) { const id = await this._searchModelIdByName(name, model_format); if (id) { model_id = id; break; } await new Promise((r) => setTimeout(r, pollIntervalMs)); } if (!model_id) throw new Error(`Model id not available after register for "${name}" with format ${model_format}`); } } const readyStart = Date.now(); while (Date.now() - readyStart < 30000) { const s = await this._getModelState(model_id); if (s) break; await new Promise((r) => setTimeout(r, 1000)); } const check = await this._getModelState(model_id); if (!check) throw new Error(`Model ${model_id} not visible yet after registration`); return model_id; } async _waitForModelRegistration(taskId, timeoutMs = 180000, pollMs = 3000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const res = await this._pluginRequest({ method: 'GET', path: `/_plugins/_ml/tasks/${encodeURIComponent(taskId)}` }); const state = res.body?.state || res.body?.task_state || res.body?.model_state; const modelId = res.body?.model_id || res.body?.result?.model_id || res.body?.worker_node?.model_id || res.body?.task_details?.model_id; if (state === 'COMPLETED' && modelId) return modelId; if (state === 'FAILED') { const reason = res.body?.error?.reason || res.body?.error || 'Model registration task failed'; throw new Error(`Model registration failed: ${reason}`); } await new Promise(r => setTimeout(r, pollMs)); } throw new Error(`Timed out waiting for model registration task ${taskId}`); } async _waitForDeployment(taskId, timeoutMs = 180000, pollMs = 3000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const res = await this._pluginRequest({ method: 'GET', path: `/_plugins/_ml/tasks/${encodeURIComponent(taskId)}` }); const state = res.body?.state || res.body?.task_state; if (state === 'COMPLETED') return; if (state === 'FAILED') { const reason = res.body?.error || res.body; throw new Error(`Model deployment failed: ${JSON.stringify(reason)}`); } await new Promise(r => setTimeout(r, pollMs)); } throw new Error(`Timed out waiting for model deployment task ${taskId}`); } async _mappingsPayload(index, workingDirectory, mappingsPath, modelId) { const properties = JSON.parse(fs.readFileSync(workingDirectory + mappingsPath, 'utf-8')); const finalProps = this._injectModelIdIntoSemanticProperties(properties, modelId); return this._client.indices.putMapping({ index, body: { properties: finalProps } }); } async _getModelState(modelId) { try { const res = await this._pluginRequest({ method: 'GET', path: `/_plugins/_ml/models/${encodeURIComponent(modelId)}`, }); return res.body?.model_state ?? res.body?.state ?? null; } catch (e) { const code = e?.meta?.statusCode; const reason = e?.meta?.body?.error?.reason || ''; if (code === 404 || /not\s*find|no such/i.test(reason)) { return null; } throw e; } } _isDeployedState(state) { return state === 'DEPLOYED' || state === 'PARTIALLY_DEPLOYED' || state === 'LOADED'; } async _deployModel(modelId) { const res = await this._client.transport.request({ method: 'POST', path: `/_plugins/_ml/models/${encodeURIComponent(modelId)}/_deploy`, }); return res?.body?.task_id ?? null; } async _waitUntilDeployed(modelId, timeoutMs, intervalMs) { const started = Date.now(); while (Date.now() - started < timeoutMs) { const state = await this._getModelState(modelId); if (this._isDeployedState(state)) return; await new Promise((r) => setTimeout(r, intervalMs)); } const last = await this._getModelState(modelId); throw new Error(`Model ${modelId} did not reach a deployed state within ${timeoutMs}ms (last state: ${last ?? 'unknown'})`); } _injectModelIdIntoSemanticProperties(obj, modelId) { if (!obj || typeof obj !== 'object') return obj; if (obj.type === 'semantic') { if (!obj.model_id && modelId) obj.model_id = modelId; return obj; } for (const k of Object.keys(obj)) { const v = obj[k]; if (Array.isArray(v)) { obj[k] = v.map((item) => this._injectModelIdIntoSemanticProperties(item, modelId)); } else if (typeof v === 'object' && v !== null) { obj[k] = this._injectModelIdIntoSemanticProperties(v, modelId); } } return obj; } _mappingsNeedModelId(obj) { if (!obj || typeof obj !== 'object') return false; if (obj.type === 'semantic' && !obj.model_id) return true; if (Array.isArray(obj)) return obj.some((v) => this._mappingsNeedModelId(v)); for (const k of Object.keys(obj)) { if (this._mappingsNeedModelId(obj[k])) return true; } return false; } async _searchModelIdByName(name, format) { const modelName = (name ?? '').trim(); if (!modelName) return null; const filters = [{ term: { 'name.keyword': modelName } }]; if (format) { filters.push({ term: { 'model_format.keyword': format } }); } const body = { query: { bool: { filter: filters, must_not: [{ exists: { field: 'chunk_number' } }], }, }, size: 1, sort: [{ created_time: { order: 'desc' } }], }; try { const res = await this._pluginRequest({ method: 'POST', path: '/_plugins/_ml/models/_search', body, }); return res.body?.hits?.hits?.[0]?._id ?? null; } catch (e) { const code = e?.meta?.statusCode; const type = e?.meta?.body?.error?.type; const reason = e?.meta?.body?.error?.reason || ''; if (code === 404 || type === 'index_not_found_exception' || /no such index/i.test(reason)) { return null; } this._throwWithServerReason('models/_search failed', e, body); } } async _resolveDeployedModelIdByName(modelName, preferred = ['ONNX', 'TORCH_SCRIPT']) { for (const fmt of preferred) { const id = await this._searchModelIdByName(modelName, fmt); if (id) { const state = await this._getModelState(id); if (state && !this._isDeployedState(state)) { const taskId = await this._deployModel(id); if (taskId) await this._waitForDeployment(taskId, 180000, 3000); await this._waitUntilDeployed(id, 180000, 3000); } return id; } } throw new Error(`Could not resolve a valid model id for "${modelName}" in formats ${preferred.join(', ')}`); } async _ensureModelGroup(name, description = '') { const groupName = (name ?? '').trim(); if (!groupName) { throw new Error('Model group name is required (got empty/undefined).'); } const body = { query: { bool: { filter: [{ term: { 'name.keyword': groupName } }], }, }, size: 1, sort: [{ created_time: { order: 'desc' } }], }; try { const search = await this._pluginRequest({ method: 'POST', path: '/_plugins/_ml/model_groups/_search', body, }); const existing = search.body?.hits?.hits?.[0]?._id ?? null; if (existing) return existing; const create = await this._pluginRequest({ method: 'POST', path: '/_plugins/_ml/model_groups/_register', body: { name: groupName, description }, }); return create.body.model_group_id; } catch (e) { const code = e?.meta?.statusCode; const type = e?.meta?.body?.error?.type; const reason = e?.meta?.body?.error?.reason || ''; if (code === 404 || type === 'index_not_found_exception' || /no such index/i.test(reason)) { const create = await this._pluginRequest({ method: 'POST', path: '/_plugins/_ml/model_groups/_register', body: { name: groupName, description }, }); return create.body.model_group_id; } this._throwWithServerReason('model_groups/_search failed', e, body); } } async _registerPretrainedModel(args) { const res = await this._pluginRequest({ method: 'POST', path: '/_plugins/_ml/models/_register', body: args, }); return res.body; } async _pluginRequest(args) { return (await this._client.transport.request(args)); } _throwWithServerReason(prefix, e, body) { const serverErr = e?.meta?.body?.error; const reason = serverErr?.root_cause?.[0]?.reason || serverErr?.reason || e?.message || String(e); const details = body ? ` | body=${JSON.stringify(body)}` : ''; throw new Error(`${prefix}: ${reason}${details}`); } } exports.OpensearchClient = OpensearchClient; //# sourceMappingURL=OpensearchClient.js.map