Optimize Workers Within Platform Limits

JFrog Platform Administration Documentation

Content Type
Administration / Platform

The Workers service has limitations that cannot be changed in JFrog Cloud environments, which were established to protect the JFrog Platform's performance and ensure smooth operation. For more information, see Workers Limitations.

Note

When reaching various Worker limits, you might get one or more of the following error messages:

  • Worker used too much memory

  • Worker used too much cpu time

  • Worker execution timeout

  • Worker waited for too long in the queue

  • Worker pool rejected the task as it is in closing state

  • The worker has been killed unexpectedly

  • Floating promises are not allowed. Make sure to await all promises

To optimize Worker performance within these limitations and avoid errors, use the following best practices when creating your Workers:

Stack external requests to get responses in bulk

If you need to send multiple external requests, instead of waiting for one promise at a time, you can organize all your requests into an array and use the Promise.allSettled function to receive a reply for all requests at once.

For example:

Original request:

for (let i=0; i < 20; ++i) {
    const result = await context.clients.platformHttp
        .get("/artifactory/api/v1/system/readiness");
    ...
}

Optimized request:

const promises = [];
for (let i=0; i < 20; ++i) {
    promises.push(
        context.clients.platformHttp.get("/artifactory/api/v1/system/readiness")
    );
}
const results = await Promise.allSettled(promises);
...

Bound your workload using states

When creating any kind of cleanup or maintenance Worker, it is common to load many resources and perform an action on each of them, but if you have a large number of resources, the Worker might return an execution timeout error.

To overcome this issue and perform actions on many resources, take the following steps:

  1. Load a limited number of resources and perform the action on them

  2. When the first group is done, mark it by adding a state: for example, by adding a property, or deleting the resource

  3. Set the Worker to skip the marked resources, and process the rest of the resources in groups.

  4. Experiment with different numbers of items processed at a time to optimize your workflow.

For example:

Original request:

const result = await context.clients.platformHttp
    .get('/artifactory/api/storage/<REPO>?list&deep=1');

for (const item of result.data.files) {
    // Do what you need
    ...
}

Optimized request:

const result = await context.clients.platformHttp
    .get('/artifactory/api/storage/<REPO>?list&deep=1');

for (const item of result.data.files) {
    const propertiesResult = await context.clients.platformHttp
        .get(`/artifactory/api/storage/<REPO>${item.uri}?properties`)
    if(propertiesResult.data.properties['DONE']?.[0] !== 'true') {
        // Do what you need
        ...
        // And once done add a marker to the file
        await context.clients.platformHttp
            .put(`artifactory/api/storage/local-gen${item.uri}?properties=DONE=true`);
    }
}

You may also use the Worker’s state to save whatever data that might be useful, as follows:

export default async (context: PlatformContext): Promise<any> => {
    const fromTimestampStr = context.state.get('timestamp');
    const fromTimestamp = Number.parseInt(fromTimestampStr) || 0;

    // Get data to handle from a given time
    const result = await context.clients.axios
        .get( `https://external.host/fetch/all?fromTimestamp=${fromTimestamp}`);

    const sortedItems = (result?.data?.files || []).sort((a, b) => a.created - b.created)

    for (const item of sortedItems) {
        // Do what you need
        ...

        // Keep track
        context.state.set('timestamp', item.created || Date.now());
    }

  return context.state.getAll();
};

Use several workers in parallel

When creating Workers, the Worker service can execute several Workers simultaneously. If a Worker is performing a heavy task, it can be worth creating several Workers and splitting the heavy task into several smaller tasks to speed up the process.

Use Short-lived tokens to avoid loading large chunks of data

When performing actions on artifacts, the Worker loads artifacts’ binary data and JSON into memory and transfers them directly, which increases memory demands and poses a possible security risk.

To avoid this, you can use a short-lived access token for the artifact URL: this method will send the artifact URL from Artifactory directly to the server and eliminating the need to load any data to the Worker memory, and avoiding the security risk of having long-lived artifact data in your Workers.

For example:

Original request:

const downloadResult = await context.clients.platformHttp.get('/artifactory/<REPO>/big-file');
const fileContent = downloadResult.data;
...

Optimized request:

// Create a short live access token (10 seconds in this example) ...
const result = await context.clients.platformHttp.post('/access/api/v1/tokens', { scope: 'applied-permissions/admin', expires_in: 10 });
const accessToken = result.data.access_token;

// ... then provide the access token and delegate the heavy memory computation to another server
await context.clients.axios.post( '<URL_TO_THE_DELEGATE_SERVER>', { accessToken, fileUrl: 'https://<plaform>/artifactory/<REPO>/big-file' });
...

Limit the number of HTTP requests

When creating an HTTP-Triggered Worker, it is common to load a list of artifacts and perform actions on each of the artifacts, each action being its own HTTP request. In this case, if a request returns 100 items, the Worker will send 1+100 HTTP requests, leading to ever-increasing execution times.

To resolve this, check our REST API documentation and see if the operation you are trying to do also accepts multiple artifacts. For example, instead of checking the properties on each artifact one by one you may use an AQL request to load only relevant items and avoid reaching a rate limit error. Then, you can use the Bound your Workload method to group your HTTP requests using state.JFrog REST APIsArtifactory Query Language

For example:

Original request:

const result = await context.clients.platformHttp
    .get('/artifactory/api/storage/<REPO>?list&deep=1');

for (const item of result.data.files) {
    const propertiesResult = await context.clients.platformHttp
        .get(`/artifactory/api/storage/<REPO>${item.uri}?properties`)
    if(propertiesResult.data.properties['DONE']?.[0] !== 'true') {
        // Do what you need
        ...
        // And once done add a marker to the file
        await context.clients.platformHttp
            .put(`artifactory/api/storage/local-gen${item.uri}?properties=DONE=true`);
    }
}

Optimized request:

// Artifactory Query Language request
const result = await context.clients.platformHttp.post(
    '/artifactory/api/search/aql',
    'items.find({"@DONE":{"$ne":"true"}, "type": "file", "repo": "<REPO>"}).limit(10)',
    { "Content-Type": 'text/plain' }
);
for (const item of result.data.files) {
    // Do what you need
    ...
    // And once done add a marker to the file
    await context.clients.platformHttp
        .put(`artifactory/api/storage/local-gen${item.uri}?properties=DONE=true`);
}

Track Current Execution Time

To overcome the execution time limit, you can keep track of it and decide to stop your processing if you are when the Worker is getting close to the limit. The method context.getRemainingExecutionTime() will return the time left (in milliseconds) before reaching the limit.

Original request:

for (const artifact of artifactsToDelete) {
    await context.clients.platformHttp.delete(`/artifatory/${artifact.path}`)
}
...

Optimized request:

// define a threshold in milliseconds for when to stop doing processes
const threshold = 250;
for (const artifact of artifactsToDelete) {
    // if the threshold is reached by execution time we break out of the loop
    if (context.getRemainingExecutionTime() < threshold) {
        break;
    }
    await context.clients.platformHttp.delete(`/artifatory/${artifact.path}`)
}
...

Limit the number of external API calls

When using an event-driven Worker such as Before Download, checking a third-party REST API can significantly increase download time and degrade user experience. This can happen even if worker limits are not reached, especially when downloading hundreds or thousands of artifacts.

To avoid this issue, you can use a property-based approach to scan the artifacts asynchronously and avoid checking external APIs every time:

  1. Create an After Create Worker which adds a property (e.g. ‘to-scan’)

  2. Get the third party to scan the artifacts asynchronously and add a property when they are scanned (e.g. ‘approved’)

  3. Create a Before Create Property Worker to prevent manual editing of the property

  4. Create a Scheduled Worker to regularly scan your artifacts for the ‘to-scan’ property, scan them, and add an ‘approved’ property when they are scanned

  5. Set the Before Download Worker to search for the property instead of checking the third-party API

Stack external requests to get responses in bulk

When logging an object, the Worker marshals (converts the data structure) of the object to a JSON string format, which can impact your Worker’s memory usage.

To resolve this, only log your Worker executions when necessary: if you are debugging, you can set the Worker to log a comment only when it is stable.

For example:

Original request:

for (let i=0; i < 20; ++i) { const result = await context.clients.platformHttp .get("/artifactory/api/v1/system/readiness"); ...
}

Optimized request:

const promises = [];
for (let i=0; i < 20; ++i) { promises.push( context.clients.platformHttp.get("/artifactory/api/v1/system/readiness") );
}
const results = await Promise.allSettled(promises);
...