Headless API

Headless Client Plugin Guide / Headless API

Overview

The Headless API allows you to create and manage headless clients dynamically. These clients can be started from other sessions to offload heavy tasks, allowing for parallel processing. This is especially useful for long-running jobs or tasks that would block normal operations in a single-threaded environment.

You can find more detailed information about Headless API here and about client methods here.

Features

  • Parallel Processing: Servoy clients are single-threaded, meaning they can only perform one task at a time. The Headless API provides a way to offload long-running tasks to another session.

  • Dynamic Client Creation: You can create headless clients programmatically, start, monitor, and shut them down as needed.

  • Job Queuing and Monitoring: Jobs can be queued for execution, and their progress can be monitored through callbacks or status checks.

Creating a client

To create a headless client, you will typically use the createClient method. This method opens a new session of a specified solution on the server, allowing you to interact with it programmatically.

Example: Creating a Headless Client

// Creates a headless client that will open the given solution.
var headlessClient = plugins.headlessclient.createClient("mySolution", "user", "pass", null);

// Check if the headless client was successfully created
if (headlessClient != null && headlessClient.isValid()) {
    application.output("Headless client successfully created.");
    // Perform headlessClient actions 

} else {
    application.output("Failed to create headless client.");
}

Getting a running client

To retrieve a running headless client in Servoy, you can use the getClient or getOrCreateClient methods from the plugins.headlessclient API. These methods allow you to access an existing headless session, enabling interaction with the client that’s already running on the server.

getClient

To retrieve a running headless client using the getClient(clientID) method, you need the clientID that uniquely identifies the headless client session. This method is helpful when you already have an active headless client on the server, and you want to reconnect to it to perform further operations.

This method does not create a new headless client—if the specified client ID doesn’t exist or the session has been closed, the method will return null.

Use Case for getClient This method is particularly useful when:

  • You have already created a headless client, and the process needs to be resumed or continued later.

  • You want to queue additional tasks or check the status of a long-running headless session.

  • The client session has to be accessed across different parts of the application or across different user sessions.

For instance, you might create a headless client to perform batch processing, and after some time, you retrieve it using its clientID to check if the task is complete or to perform more actions.

Example: Retrieving a Running Headless Client

// Retrieve the headless client using the client ID.
var headlessClient = plugins.headlessclient.getClient("ClientID"); // ClientID: This is the unique identifier of the headless client you want to retrieve. It is generated when the headless client is created

// Check if the client is successfully retrieved and still valid
if (headlessClient != null && headlessClient.isValid()) {
    application.output("Headless client successfully retrieved.");
    // Interact with the existing headless client

} else {
    // Handle the case where the client is not found or is invalid
    application.output("No client found with the given ID.");
}

getOrCreateClient

The getOrCreateClient method in Servoy is used to either retrieve an existing headless client using its unique clientID or create a new one if no client with the specified ID is found. This method is ideal when you need to ensure that a headless client session is available, whether it already exists or needs to be created.

If a headless client with the specified clientID exists and is running with the provided solution, it will return that client. If no such client exists, the method will create a new headless client for the specified solution and return it.

Use Case for getOrCreateClient This method is particularly useful when:

  • You are uncertain whether a specific headless client is already running, but you want to ensure that the client is either retrieved or created to perform some tasks.

  • You want to persist a specific session across different operations (e.g., for background processes, integration tasks, or automation) by reusing a clientID.

  • You need a long-running or reusable headless client session that can be accessed and shared by different parts of your solution or after a server restart.

Example: Retrieving or Creating a Headless Client

// Unique identifier for the headless client, which could be stored and reused
var clientID = "storedClientID";

// Attempt to retrieve or create the headless client
var headlessClient = plugins.headlessclient.getOrCreateClient(clientID, "mySolution", "user", "pass", null);

// Check if the client was successfully retrieved or created, and ensure it is valid
if (headlessClient != null && headlessClient.isValid()) {
    application.output("Headless client successfully retrieved.");
    // Interact with the existing or newly created headless client

} else {
    // Handle the case where the client could not be retrieved or created
    application.output("Failed to retrieve or create the headless client.");
}

Difference between getClient and getOrCreateClient

The key difference between getClient and getOrCreateClient lies in their behavior when a headless client with the specified clientID does not exist. Here’s a detailed breakdown of how each method operates:

  • getClient

    • Purpose: The getClient method is used solely to retrieve an existing headless client.

    • Behavior:

      • If a headless client with the specified clientID is running, it returns that client.

      • If no client with the given clientID is found, it returns null.

    • Use Case: Use getClient when you know that a headless client already exists, and you want to reconnect or continue interacting with it. This method does not create a new client if the specified one doesn't exist.

  • getOrCreateClient

    • Purpose: The getOrCreateClient method can either retrieve an existing client or create a new one.

    • Behavior:

      • If a headless client with the specified clientID already exists for the specified solution, it returns that client.

      • If no client with the given clientID exists, it creates a new headless client using the provided solutionName, username, password, and solutionOpenMethodArgs.

    • Use Case: Use getOrCreateClient when you want to ensure that a client exists for the given clientID. If it’s not running already, this method will create a new one, making it useful in situations where you want to avoid checking whether a client exists before creating it.

Key Differences:

Feature

getClient

getOrCreateClient

Primary Function

Retrieves an existing headless client

Retrieves an existing client or creates a new one if none exists

When Client Does Not Exist

Returns null

Creates a new client with the specified parameters

Error Handling

Requires handling the case where the client does not exist (returns null)

Automatically handles missing clients by creating a new session

Solution Flexibility

Only retrieves an existing client, no solution-specific behavior

Can load a new solution if a client doesn’t exist for the given ID

Use Case

Best for reconnecting to an existing headless client

Best when you want to ensure a client is available, whether it exists or not

Reusing a fixed clientID vs creating a unique (UUID)

When working with Servoy headless clients, you have the option to either reuse a fixed clientID or generate a unique clientID (UUID) for each new client session. Both approaches have their advantages and use cases depending on the nature of your application, the longevity of the headless client sessions, and how you intend to interact with those sessions.

  1. Reusing a Fixed clientID

Overview:

  • In this approach, you use a predefined, fixed clientID for a specific headless client session.

  • The same clientID is reused across multiple sessions or interactions, ensuring that the same headless client session can be retrieved and continued over time.

Benefits:

  • Session Continuity: By reusing a fixed clientID, you ensure that the headless client session can be persisted and accessed repeatedly, even after restarts. This is ideal for long-running processes where the client must be revisited later to resume operations or access its state.

  • Resource Efficiency: Reusing a fixed clientID prevents creating multiple headless clients for the same task. Instead of starting a new session each time, you can retrieve and reuse the existing client, which saves server resources.

  • State Preservation: If your headless client maintains important state or context (such as working with foundsets, data processing, etc.), using a fixed clientID allows you to retrieve that session and continue where you left off.

Use Case:

  • Ideal for long-running processes that may need to be resumed over time (e.g., batch data processing, scheduled tasks, or background jobs).

  • Useful when multiple systems or components need to interact with the same session or share client data.

Considerations:

  • Single-Session Limitation: If the same clientID is reused in multiple places or by different users, it can lead to conflicts or unexpected behavior, as there will only be one session associated with that ID.

  • Concurrency: Only one instance of the headless client will be running for the given clientID, which could be a limitation if multiple processes need to run independently.

Example:

var clientID = "fixedClientID";  // Predefined ID stored in the system
var headlessClient = plugins.headlessclient.getOrCreateClient(clientID, "mySolution", "user", "pass", null);

if (headlessClient != null && headlessClient.isValid()) {
    // Continue working with the fixed client session
    headlessClient.queueMethod(null, "longRunningTask", [param1], callback);
}
  1. Creating a Unique clientID (UUID)

Overview:

  • In this approach, you generate a unique identifier (UUID) for each new headless client session. UUIDs are universally unique and ensure that every client session is completely independent from others.

  • This is usually done by creating a new clientID each time a headless client is instantiated, ensuring that no session collides with another.

Benefits:

  • Isolation of Sessions: Each headless client gets its own unique session, which prevents any interference between different processes. This is important when tasks need to run independently without affecting each other.

  • Concurrency: You can run multiple independent headless clients in parallel, each with its own UUID, enabling concurrent processing. Each session will have its own state, and multiple clients can perform tasks simultaneously without conflict.

  • Automatic Cleanup: If your system doesn’t require long-term persistence of the client session, using UUIDs ensures that each session is isolated and can be easily discarded when no longer needed.

Use Case:

  • Ideal for short-lived or one-off tasks that don’t need to persist across sessions (e.g., processing user-specific requests, running temporary background tasks, handling concurrent jobs).

  • Useful when you need to run multiple, independent headless client sessions simultaneously.

Considerations:

  • Resource Usage: Since each UUID generates a new client session, you might use more server resources as multiple headless clients are created and managed simultaneously.

  • No Continuity: Unlike fixed clientIDs, sessions created with a UUID typically don’t have long-term persistence. Once the session ends, the client is usually discarded, meaning you cannot revisit that session later.

  • Session Management: You need to ensure that unused sessions are properly cleaned up (e.g., using shutdown()), especially in cases where many UUIDs are generated.

Example:

var headlessClient = plugins.headlessclient.createClient("mySolution", "user", "pass", null);
var ClientID;
if (headlessClient != null && headlessClient.isValid()) {
    // Store the generated UUID (clientID) somewhere to reference this session later
    ClientID = headlessClient.getClientID();

    // Perform operations with the unique client session
    headlessClient.queueMethod(null, "processData", [param1], callback);
}

Summary of Key Differences:

Feature

Fixed clientID

Unique clientID (UUID)

Session Continuity

Provides a reusable session across multiple requests

Creates a new session for each client, no continuity

Use Case

Long-running tasks, persistence of state

One-off or short-lived tasks, no need for continuity

Concurrency

Limited to a single session per clientID

Allows concurrent sessions with separate UUIDs

Resource Efficiency

Reuses the same client session, saving resources

Creates new sessions, higher resource usage

Session Isolation

Single session reused by multiple components

Independent, isolated sessions for each task

State Preservation

Maintains the client’s state for future use

Each session is independent, no state preservation

Potential for Conflicts

Could cause conflicts if multiple processes share the same clientID

No conflicts as each client has a unique ID

When to Use:

  • Reusing a fixed clientID is best when you need session persistence and plan to revisit the same client session over time. It is ideal for background processes, long-running tasks, or shared headless sessions.

  • Creating a unique clientID (UUID) is useful when tasks need to run independently, or you are handling short-lived, one-time processes. It is perfect for cases where multiple clients need to run concurrently, with each having its own isolated session.

Running/scheduling on startup

Running or scheduling headless clients on startup in Servoy can be an effective way to initiate background processes, such as data synchronization, batch processing, or scheduled tasks, as soon as the application server or solution starts. Here’s how you can automate headless client operations to run on startup.

Running a headless client on startup

The simplest way to run a headless client on startup is by placing the headless client creation logic inside the solution's onSolutionOpen method. This method runs automatically when the solution starts and can be used to kick off background processes.

Example: Starting a Headless Client in the onOpen Method

function onSolutionOpen() {
    // Automatically create and start a headless client when the solution starts
    var headlessClient = plugins.headlessclient.createClient("mySolution", "admin", "password", null);

    if (headlessClient != null && headlessClient.isValid()) {
        // Queue a method to be executed by the headless client on startup
        headlessClient.queueMethod(null, "initializeBackgroundTasks", null);
        application.output("Headless client started and background task initialized.");
    } else {
        application.output("Failed to create the headless client.");
    }
}

In this approach:

  • A headless client is created automatically when the solution starts.

  • The initializeBackgroundTasks method is queued for execution by the headless client.

Scheduling a headless client on startup

If you want to schedule headless clients to run at regular intervals (including on startup), you can use the Scheduler Plugin. You can schedule a headless client to start immediately after the solution loads, or at a specific time.

Example: Scheduling a Headless Client with the Scheduler

function onSolutionOpen() {
    // Schedule a headless client to run on startup
    plugins.scheduler.addCronJob('startupHeadlessClient', '0 0 * * * ?', startHeadlessClientOnStartup); // Runs hourly

    application.output("Scheduled headless client task on startup.");
}

function startHeadlessClientOnStartup() {
    var headlessClient = plugins.headlessclient.createClient("mySolution", "admin", "password", null);

    if (headlessClient != null && headlessClient.isValid()) {
        headlessClient.queueMethod(null, "executePeriodicTask", null);
        application.output("Headless client started with periodic task.");
    }
}

In this approach:

  • A cron job is scheduled using addCronJob, which will trigger the startHeadlessClientOnStartup function at the specified time (in this case, hourly).

  • The headless client is created within the scheduled job, and a method is queued for execution.

Queuing a job

Queuing a job in Servoy, especially within a headless client, is a common task used to perform background operations without interrupting the main application flow. The method queueMethod() is used to queue jobs for headless clients, allowing for asynchronous execution of server-side logic, such as data processing, external API calls, or other long-running tasks.

When multiple jobs are queued, they will be executed sequentially in the order they are added to the queue.

Example: Queuing Multiple Jobs

// Create a headless client
var headlessClient = plugins.headlessclient.createClient("mySolution", "admin", "password", null);

if (headlessClient != null && headlessClient.isValid()) {
    // Queue multiple methods to execute on the server without callback
    headlessClient.queueMethod(null, "processJob1", [param1]);
    headlessClient.queueMethod(null, "processJob2", [param2]);
    headlessClient.queueMethod(null, "processJob3", [param3]);

    // Optionally log that the jobs have been queued
    application.output("Multiple jobs queued for execution.");
} else {
    application.output("Failed to create the headless client.");
}

Steps in the Example:

  1. Create a Headless Client: The headless client is created using createClient. If you have an existing client, you can use getClient or getOrCreateClient to retrieve it.

  2. Queue Multiple Jobs: The queueMethod() is used to queue multiple methods (processJob1, processJob2, processJob3) for execution on the server. Each method is passed a set of parameters (in this case, param1, param2, param3). No callback is provided, meaning the job runs in the background, and the result or any exceptions are not tracked.

  3. Execution Order: Jobs are executed in the order they are queued. In this case:

    • processJob1 will execute first.

    • After that, processJob2 will run.

    • Finally, processJob3 will execute.

  4. Optional Logging: Logging the job queue status with application.output() helps track that the jobs were queued successfully.


Example: Queuing Jobs with Complex Parameters If the methods require complex parameters (e.g., objects or arrays), you can pass them in the queueMethod() call:

var job1Params = { id: 1, action: "update", data: { name: "John", age: 30 } };
var job2Params = { id: 2, action: "delete", data: null };
var job3Params = { id: 3, action: "create", data: { name: "Doe", age: 25 } };

// Queue the jobs with complex parameters
headlessClient.queueMethod(null, "processJob1", [job1Params]);
headlessClient.queueMethod(null, "processJob2", [job2Params]);
headlessClient.queueMethod(null, "processJob3", [job3Params]);

application.output("Jobs with complex parameters queued without callback.");

Handling the callback

Handling the callback in Servoy when queuing jobs in a headless client is important when you need to track the result of the background task, manage errors, or take further actions based on the outcome of the job. The queueMethod() function allows you to define a callback method that is triggered once the job is completed, giving you the flexibility to manage success or handle any exceptions that occur during execution.

When queuing a method using queueMethod(), you can provide a callback function that will be invoked after the method completes its execution on the server. The callback function receives a JSEvent object that provides information about the execution, including whether it succeeded or failed.

Example: Handling the Callback

// Create a headless client
var headlessClient = plugins.headlessclient.createClient("mySolution", "admin", "password", null);

if (headlessClient != null && headlessClient.isValid()) {
    // Queue a job with a callback function to handle the result
    headlessClient.queueMethod(null, "processJob", [param1], jobCallback);

    application.output("Job queued for execution.");
} else {
    application.output("Failed to create or retrieve the headless client.");
}

// Callback function to handle the result of the job
function jobCallback(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        // Handle success
        application.output("Job completed successfully. Data: " + event.data);
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        // Handle error
        application.output("An error occurred during job execution: " + event.data);
    }
}

Key Components:

  1. Queuing the Job:

    • The queueMethod() method is used to queue the processJob method for execution.

    • The last argument in the method is the jobCallback function, which will be invoked once the job is completed.

  2. The Callback Function:

    • The callback function receives a JSEvent object.

    • You can use event.getType() to check if the job completed successfully or if an error occurred.

    • There are two possible outcomes:

  3. Handling Success:

  4. Handling Errors:

    • If an exception occurs during the job execution, event.getType() will return JSClient.CALLBACK_EXCEPTION_EVENT.

    • The error message or exception details will be available in event.data.

    • You can log the error or trigger corrective actions based on this information.


Example: Handling Multiple Jobs with Different Callbacks You can queue multiple jobs with different callback functions to handle each job’s result individually.

// Queue multiple jobs with different callbacks
headlessClient.queueMethod(null, "processJob1", [param1], callbackForJob1);
headlessClient.queueMethod(null, "processJob2", [param2], callbackForJob2);

// Callback for the first job
function callbackForJob1(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job 1 completed successfully. Data: " + event.data);
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("Job 1 failed with error: " + event.data);
    }
}

// Callback for the second job
function callbackForJob2(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job 2 completed successfully. Data: " + event.data);
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("Job 2 failed with error: " + event.data);
    }
}

In this example:

  • Job 1 and Job 2 are queued with different parameters and different callback functions.

  • Each callback function independently handles the result or error from the respective job.


Example: Processing Data from the Callback If the queued method returns data, you can process it directly within the callback function. For example, if the job returns a processed list of data, you can handle it as follows:

function jobCallback(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        var resultData = event.data;  // Assuming data is returned as an array
        for (var i = 0; i < resultData.length; i++) {
            application.output("Processed Data: " + resultData[i]);
        }
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("Error occurred: " + event.data);
    }
}

In this case:

  • The method executed by the headless client returns a list of processed data.

  • The callback function iterates over the returned data and outputs it.

Important Considerations:

  • Error Handling: Always include logic to handle both successful completion and errors in your callback function. This ensures you can manage issues such as method failures or unexpected exceptions.

  • Execution Time: Keep in mind that the job execution on the server is asynchronous, so the callback may be triggered at any time. Make sure the callback function is prepared to handle the result at a later point in time.

  • Result Processing: If the method executed by the headless client returns data (through event.data), make sure you understand the format of the data so that it can be processed correctly in the callback.

  • Chaining Jobs: If the result of one job triggers another job or process, you can queue the next method from within the callback function.


Example: Chaining Jobs via Callbacks

function jobCallback(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job completed successfully. Starting the next job...");
        headlessClient.queueMethod(null, "nextJob", [event.data], nextJobCallback);
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("An error occurred: " + event.data);
    }
}

// Callback for the next job
function nextJobCallback(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Next job completed successfully.");
    }
}

In this example:

  • After the first job completes, the callback queues another job (nextJob) and passes the result of the first job as an argument.

  • The second job is handled by its own callback function.

Getting the status of a running client

When managing Servoy headless clients, it’s crucial to monitor the status of each client to ensure that jobs are progressing as expected. There are various methods to track the status of a client, including checking the validity of the client, tracking job completion using callbacks, scheduling periodic status checks, and retrieving real-time data from the client using getDataProviderValue().

Checking Client Validity

The isValid() method is used to check whether a headless client session is still active and valid. This method returns true if the client is active and can process jobs, and false if the session has expired or the client has been shut down.

Example: Checking Client Validity

// Retrieve an existing headless client using its clientID
var headlessClient = plugins.headlessclient.getClient("storedClientID");

if (headlessClient != null) {
    if (headlessClient.isValid()) {
        application.output("Headless client is still running and valid.");
    } else {
        application.output("Headless client session is no longer valid.");
    }
} else {
    application.output("No headless client found with the given ID.");
}

In this example, the system checks whether the client is still valid and running. If the client is not valid, it logs that the session has ended.

Tracking Job Progress with Callbacks

Callbacks are used to track the completion of jobs that are queued for a headless client. When you queue a job using queueMethod(), you can provide a callback function that will be triggered upon job completion, indicating whether it succeeded or failed.

Example: Tracking Job Completion with Callbacks

// Queue a job with a callback function to handle the result
headlessClient.queueMethod(null, "processJob", [param1], jobCallback);

// Callback function to handle the job result
function jobCallback(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job completed successfully. Data: " + event.data);
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("An error occurred during job execution: " + event.data);
    }
}

In this example, the jobCallback function handles the result of the job. If the job completes successfully, it logs the data returned. If an error occurs, it logs the error message.

Retrieving Real-Time Data

The getDataProviderValue() method allows you to retrieve the value of a data provider (e.g., a field or variable) from the context of the headless client. This is particularly useful when you need to monitor or track specific data during the execution of a job, such as a job’s progress or the status of a key variable.

Example: Retrieving a Data Provider Value

if (headlessClient != null && headlessClient.isValid()) {
    // Retrieve the value of a global variable in the headless client's context
    var jobProgress = headlessClient.getDataProviderValue(null, "scopes.globals.jobProgress");

    if (jobProgress != null) {
        application.output("Job progress: " + jobProgress + "%");
    } else {
        application.output("Failed to retrieve job progress.");
    }
}

In this example:

  • The getDataProviderValue() method is used to retrieve the value of scopes.globals.jobProgress, which could represent the progress of a long-running job.

  • null is passed as the context, indicating that this is a global variable.

  • If the data provider value is successfully retrieved, the current job progress is logged. If the value is null, an error message is logged.


Retrieving Data Provider Value in a Specific Form Context

If the data provider is tied to a specific form or form field, you can retrieve the value within that form’s context by providing the form name as the first argument.

if (headlessClient != null && headlessClient.isValid()) {
    // Retrieve a data provider value from a specific form context
    var formData = headlessClient.getDataProviderValue("myForm", "myDataProvider");

    if (formData != null) {
        application.output("Form data: " + formData);
    } else {
        application.output("Failed to retrieve form data.");
    }
}

In this example:

  • The value of a form’s data provider (myDataProvider in myForm) is retrieved.

  • This could represent a field value within a specific form that the headless client is processing.

Periodic Status Checks with a Scheduler

For long-running processes, you can periodically check the status of headless clients using Servoy's scheduler. This allows you to monitor whether clients are still active and, if necessary, take action (e.g., restart the client or log issues).

Example: Scheduling Periodic Status Checks This example sets up a scheduled task that runs every hour to check if the headless client is still valid. If the client is no longer valid, the scheduled task is stopped:

// Schedule a job to periodically check the status of the headless client
plugins.scheduler.addCronJob('checkClientStatus', '0 * * * * ?', checkClientStatus);  // Runs every hour

function checkClientStatus() {
    var headlessClient = plugins.headlessclient.getClient("storedClientID");

    if (headlessClient != null) {
        if (headlessClient.isValid()) {
            application.output("Headless client is still valid.");
        } else {
            application.output("Headless client session has ended.");
            plugins.scheduler.removeJob('checkClientStatus');  // Stop the scheduled job if the client is no longer valid
        }
    } else {
        application.output("No headless client found with the given ID.");
        plugins.scheduler.removeJob('checkClientStatus');  // Stop the job if the client is not found
    }
}

Handling Client Failures and Inactivity

If a headless client becomes invalid or fails during a job, it’s important to handle these cases gracefully.

Example: Handling Client Failures

function jobCallback(event) {
    var client = event.getClient();

    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job completed successfully for client: " + client.getClientID());
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("Job failed for client: " + client.getClientID() + " with error: " + event.data);

        // Handle the client failure, such as reassigning the job to another client
        requeueJob(event);
    }
}

In this example:

  • If the job fails, the failure is logged, and the job can be requeued for another client to handle.

Shutdown a client

Shutting down a headless client in Servoy is essential for properly managing system resources, especially when the client is no longer needed or when its tasks are complete. Shutting down a headless client will free up server resources and prevent unnecessary processes from running in the background.

To shut down a headless client, you use the shutdown() method of the headless client object. This method terminates the client session and releases any resources it was using. You can also use the shutdown(force) method to forcefully close the client, if necessary.

Example: Shutting Down a Headless Client

// Retrieve an existing headless client using its clientID
var headlessClient = plugins.headlessclient.getClient("storedClientID");

if (headlessClient != null && headlessClient.isValid()) {
    // Shut down the client gracefully
    headlessClient.shutdown();
    application.output("Headless client has been shut down.");
} else {
    application.output("Headless client is not valid or already shut down.");
}

Steps in the Example:

  1. Retrieve the Headless Client: Use the getClient(clientID) method to retrieve an existing headless client using its clientID.

  2. Check Validity: Before shutting down the client, use isValid() to ensure that the client is still active. If the client is not valid (e.g., already expired or shut down), there's no need to attempt a shutdown.

  3. Shutdown the Client: The shutdown() method gracefully terminates the headless client, releasing all associated resources.


Example: Shutting Down a Client After Completing a Job If you want to shut down the client after it has completed its tasks, you can do so in the callback of the queued method.

// Queue a job and shutdown the client after it's completed
headlessClient.queueMethod(null, "processJob", [param1], function(event) {
    if (event.getType() == JSClient.CALLBACK_EVENT) {
        application.output("Job completed successfully.");
    } else if (event.getType() == JSClient.CALLBACK_EXCEPTION_EVENT) {
        application.output("An error occurred during job execution.");
    }

    // Shut down the client after the job is complete
    headlessClient.shutdown();
    application.output("Headless client has been shut down after completing the job.");
});

Example: Shutting Down Multiple Clients If you have multiple headless clients running and you want to shut them down in bulk, you can iterate over the client IDs and shut each one down.

// List of client IDs
var clientIDs = ["clientID1", "clientID2", "clientID3"];

for (var i = 0; i < clientIDs.length; i++) {
    var headlessClient = plugins.headlessclient.getClient(clientIDs[i]);
    
    if (headlessClient != null && headlessClient.isValid()) {
        headlessClient.shutdown();
        application.output("Headless client " + clientIDs[i] + " has been shut down.");
    } else {
        application.output("Client " + clientIDs[i] + " is not valid or already shut down.");
    }
}

Forcefully Shutting Down a Client

If you need to forcefully shut down the client (e.g., if the client is stuck or unresponsive), you can use the shutdown(force) method. This method attempts to close the client regardless of whether it is in the middle of a task.

// Forcefully shut down the client
headlessClient.shutdown(true);
application.output("Headless client has been forcefully shut down.");

When to Use shutdown() vs shutdown(true):

  • shutdown(): This is the preferred method for gracefully terminating a client, allowing it to finish any ongoing tasks or cleanup operations before shutting down.

  • shutdown(true): Use this when you need to forcefully terminate a client, especially in cases where the client is unresponsive or stuck in a long-running operation. Be cautious with this approach, as it may interrupt important processes.

Key Considerations:

  • Resource Management: Shutting down a headless client when it's no longer needed ensures efficient use of server resources. Leaving unused clients running can lead to performance issues or resource exhaustion.

  • Graceful Shutdown: Use shutdown() to allow the client to finish its work before shutting down, ensuring that no data is lost or processes are interrupted.

  • Forced Shutdown: Only use shutdown(true) if absolutely necessary, as it may terminate processes mid-execution, which could lead to inconsistent data or unfinished tasks.

Advanced: Running many jobs

Using Servoy headless clients to build a complex job queueing system allows you to efficiently manage and execute multiple background tasks, handle concurrency, and ensure scalability. Headless clients in Servoy are ideal for performing tasks without a user interface, making them well-suited for running jobs like data processing, batch operations, or integrations with external systems.

Here's a conceptual guide on how you could design and implement a complex job queuing system using Servoy headless clients:

  1. Understand the Components of a Job Queueing System: To build a job queuing system using headless clients, you need to consider the following key components:

    • Job Queue: A place to store jobs that need to be executed. Jobs can include tasks such as data processing, API calls, or other business logic.

    • Headless Client Pool: A pool of reusable headless clients that can be assigned to execute jobs from the queue.

    • Scheduler: A mechanism to schedule jobs at specific intervals or in response to events.

    • Job Status Tracking: A way to track whether a job has been successfully executed, is in progress, or has failed.

    • Concurrency Management: Ensuring that multiple jobs can be executed concurrently without overloading the system. Job Prioritization: The ability to handle critical jobs before less important ones.

  2. Setting Up a Job Queue: The job queue is the core of your system, where all jobs are stored before being processed. Each job in the queue typically contains:

    • Job ID: A unique identifier for the job.

    • Job Type: The specific type of job to be executed (e.g., data import, export, report generation).

    • Parameters: Any parameters or input data required for the job.

    • Priority: A priority level that determines the order in which jobs are processed.

    • Status: The current status of the job (e.g., pending, running, completed, failed). The queue can be implemented in various ways, such as a database table where each job is stored along with its attributes. You can also keep jobs in memory if persistence isn’t required.

  3. Client Pool for Parallel Processing: You will need a pool of headless clients to process jobs in parallel. The client pool is a collection of pre-initialized headless clients that can be assigned jobs from the queue.

    • Client Initialization: When the system starts, you create a set number of headless clients (based on the expected load) and store them in a pool.

    • Reusability: Once a client finishes a job, it becomes available for the next job. This avoids the overhead of creating and destroying clients repeatedly.

    • Dynamic Scaling: You can dynamically adjust the pool size based on the system's load by creating more clients during peak hours or reducing them during low usage.

  4. Job Assignment and Execution: Jobs are assigned to available clients from the pool. The system continuously monitors the pool for available clients, and when a client becomes free, it is assigned the next job in the queue.

    • Job Prioritization: Higher-priority jobs should be handled first, so the system can sort the job queue by priority.

    • Concurrency Control: Ensure that multiple jobs can run in parallel, but also prevent overloading the system by limiting the number of concurrent jobs.

  5. Handling Job Execution with Callbacks: Each job should be executed by the headless client asynchronously using callbacks. This allows you to track the status of the job (success or failure) and decide what to do next:

    • Job Success: If the job completes successfully, update the job's status in the queue and log the result.

    • Job Failure: If the job fails, retry the job or escalate the issue (e.g., send notifications or logs).

    • Chaining Jobs: You can chain jobs by queuing the next job after the current one completes.

  6. Job Scheduling: Some jobs might need to be executed at specific times (e.g., nightly reports or hourly data synchronization). You can integrate Servoy’s scheduler to schedule jobs at regular intervals.

    • Recurring Jobs: Use the scheduler to automatically queue jobs at scheduled intervals (e.g., daily, weekly).

    • Event-Driven Scheduling: Trigger jobs in response to specific events (e.g., data updates or user actions).

  7. Job Status Tracking: To effectively manage a job queueing system, you need to track the status of each job:

    • Pending: Job is in the queue but not yet started.

    • Running: Job is currently being executed by a headless client.

    • Completed: Job has successfully finished execution.

    • Failed: Job failed during execution, and error handling needs to be performed. You can store job statuses in a database or in-memory store, depending on the level of persistence you require. Tracking helps ensure that jobs don’t get lost, and it allows for retry mechanisms if a job fails.

  8. Error Handling and Job Retries: In a complex system, some jobs may fail due to external issues (e.g., network errors, database connection issues). Implement robust error handling to catch these failures and decide whether to:

    • Retry the job: Attempt to run the job again after a short delay.

    • Escalate the issue: Send a notification or log the error for manual intervention. By implementing retries, you can handle transient failures and increase the reliability of your system.

  9. Monitoring and Scaling: As the system grows, you need to monitor performance and scale accordingly:

    • System Monitoring: Monitor the number of jobs processed, job failures, and system resource usage (e.g., memory, CPU). This helps in identifying bottlenecks.

    • Scaling the Client Pool: Dynamically increase or decrease the number of clients in the pool based on job demand. For example, during peak hours, you may want to spin up more clients to handle the increased load.

  10. Advanced Features: Throttling and Batching: In some cases, you may want to throttle the number of jobs being processed simultaneously to prevent overwhelming the system. You might also want to batch jobs together if they share similar characteristics or need to be executed sequentially.

    • Throttling: Set a limit on the number of concurrent jobs to ensure that your system doesn’t run out of resources.

    • Batch Processing: Queue related jobs together and process them in groups to reduce overhead and improve efficiency.

Last updated