@kuflow/kuflow-temporal-worker
Version:
Worker library used by KuFlow SDKs and Temporal.
188 lines (152 loc) • 6.02 kB
text/typescript
/**
* The MIT License
* Copyright © 2021-present KuFlow S.L.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import type { FullOperationResponse } from '@azure/core-client'
import type { Worker } from '@kuflow/kuflow-rest'
import { Runtime } from '@temporalio/worker'
import os from 'os'
import type { KuFlowTemporalConnectionOptions } from './kuflow-temporal-connection-options'
export interface KuFlowWorkerInformationNotifierBackoff {
/**
* Seconds between errors, default 1 second
*/
sleep?: number
/**
* Exponential rate applied if the error persists, default 2.5
*/
exponentialRate?: number
}
export interface KuFlowWorkerInformation {
identity: string
taskQueue: string
workflowTypes: string[]
activityTypes: string[]
}
export interface KuFlowWorkerInformationNotifierParams {
workerInformation: KuFlowWorkerInformation
options: KuFlowTemporalConnectionOptions
}
const HEADER_X_KF_DELAY_WINDOW = 'x-kf-delay-window'
export class KuFlowWorkerInformationNotifier {
private readonly backoff: Required<KuFlowWorkerInformationNotifierBackoff>
private started = false
private delayWindowInSeconds = 5 * 60 // 5 min
private consecutiveFailures = 0
private scheduleCreateOrUpdateWorkerTimeout?: NodeJS.Timeout
public static instance(params: KuFlowWorkerInformationNotifierParams): KuFlowWorkerInformationNotifier {
return new KuFlowWorkerInformationNotifier(params.workerInformation, params.options)
}
private constructor(
private readonly workerInformation: KuFlowWorkerInformation,
private readonly options: KuFlowTemporalConnectionOptions,
) {
const backoff = this.options.kuflow.workerInformationNotifierBackoff
this.backoff = {
sleep: backoff?.sleep ?? 1,
exponentialRate: backoff?.exponentialRate ?? 2.5,
}
}
public async start(): Promise<void> {
if (this.started) {
return
}
await this.createOrUpdateWorker()
this.scheduleCreateOrUpdateWorker()
this.started = true
}
public async close(): Promise<void> {
if (!this.started) {
return
}
this.scheduleCreateOrUpdateWorkerTimeout != null && clearTimeout(this.scheduleCreateOrUpdateWorkerTimeout)
this.scheduleCreateOrUpdateWorkerTimeout = undefined
this.started = false
}
private async createOrUpdateWorker(): Promise<void> {
const workerRequest: Worker = {
identity: this.workerInformation.identity,
ip: this.getIPAddress(),
hostname: os.hostname(),
taskQueue: this.workerInformation.taskQueue,
workflowTypes: this.workerInformation.workflowTypes,
activityTypes: this.workerInformation.activityTypes,
tenantId: this.options.kuflow.tenantId,
installationId: this.options.kuflow.installationId,
robotIds: this.options.kuflow.robotIds,
}
const workerOperations = this.options.kuflow.restClient.workerOperations
try {
let rawResponse: FullOperationResponse | undefined = undefined as FullOperationResponse | undefined
const workerResponse = await workerOperations.createWorker(workerRequest, {
onResponse: rawResponseInner => {
rawResponse = rawResponseInner
},
})
Runtime.instance().logger.info(
`Registered worker ${workerResponse.taskQueue}/${workerResponse.identity} with id ${workerResponse.id}`,
)
this.consecutiveFailures = 0
const delayWindowHeader = rawResponse?.headers.get(HEADER_X_KF_DELAY_WINDOW)
if (delayWindowHeader != null) {
this.delayWindowInSeconds = parseInt(delayWindowHeader, 10)
}
} catch (error) {
Runtime.instance().logger.error(
`There are some problems registering worker ${workerRequest.taskQueue}/${workerRequest.identity}`,
{ error },
)
this.consecutiveFailures++
}
}
private scheduleCreateOrUpdateWorker(): void {
let delayInSeconds = this.delayWindowInSeconds
if (this.consecutiveFailures > 0) {
delayInSeconds = Math.round(
Math.min(delayInSeconds, this.backoff.sleep * Math.pow(this.backoff.exponentialRate, this.consecutiveFailures)),
)
}
this.scheduleCreateOrUpdateWorkerTimeout != null && clearTimeout(this.scheduleCreateOrUpdateWorkerTimeout)
this.scheduleCreateOrUpdateWorkerTimeout = setTimeout(() => {
this.createOrUpdateWorker().then(
() => {
this.scheduleCreateOrUpdateWorker()
},
() => {
this.scheduleCreateOrUpdateWorker()
},
)
}, delayInSeconds * 1_000)
}
private getIPAddress(): string {
const interfaces = os.networkInterfaces()
for (const devName in interfaces) {
const iface = interfaces[devName] ?? []
for (let i = 0; i < iface.length; i++) {
const alias = iface[i]
if (alias.address !== '127.0.0.1' && alias.address !== '0:0:0:0:0:0:0:1' && !alias.internal) {
return alias.address
}
}
}
return '0.0.0.0'
}
}