Qorus Integration Engine®  5.1.31_git
Implementing Services

Overview

All Qorus integration objects are described by YAML metadata produced by our IDE, which is available as a free (as in free of charge and also open source) extension to Microsoft Visual Studio Code, a multi-platform editor from Microsoft. The Qorus extension is called Qorus Developer Tools and can be installed directly from Visual Studio Code.

User services are like named and versioned API sets that can be live-upgraded. User service methods are available to all workflows and jobs (and to other services) and can be automatically exported to other applications through lightweight web services (RPC protocols and REST) through the HTTP server (in fact, this is the default behavior, but can be inhibited by setting the internal flag on the service method).

Services in the Qorus IDE

Additionally, services can also have one or more background threads, and therefore do not have to wait for input data to perform some processing.

See also

Services can also be called from the command-line using the qrest program as follows:

unixprompt% qrest put services/<name>/<method>/call args=... 

Qorus Services

Services are defined in YAML in files defining service metadata including an attribute giving the file name of the service class source that implements the method logic.

Services are loaded from the database into their own program objects and have their own internal API. All user-defined services must be of the type "user". System services are delivered with Qorus and should not be modified, as this is likely to affect system stability.

The following diagram illustrates the a subset of the attributes of a service.

Service Metadata Diagram

Service Metadata

Key Mand.? Description
Service Type Y The type of service: system or user
Service Name Y The name of the service; generally considered unique
Service Version Y The version of the service; only one version of a service can be loaded at a time
Service Patch N A string that describes the patchlevel of the service
Service Description Y The description of the service; supports markdown in the UI and the IDE
Service Author N The author of the service
Service Library N Library objects providing additional code to the service
Service Mappers N A list of data mappers that the service can use
Service Value Maps N A list of value maps that the service can use
Service Language Y The language of the service's source code
Service Source Y The source code to the service
Service Methods N The methods implemented by the service's source code and/or base classes
Service Configuration Items N Configuration for the service
Service File Resources N File resources for the service
Service Autostart Flag N The autostart flag that indicates if the service should be started automatically
Service Remote Flag N The remote flag that indicates if the service runs in its own process or not
Stateless Service Kubernetes Initial CPU Requirements N The initial CPU allocation for stateless services running in Qorus in Kubernetes
Stateless Service Kubernetes Initial Memory Requirements N The initial memory requirement for stateless services running in Qorus in Kubernetes
Stateless Service Kubernetes CPU Usage Limit N The CPU usage limit for stateless services running in Qorus in Kubernetes
Stateless Service Kubernetes Memory Limit N The hard memory limit for stateless services running in Qorus in Kubernetes
Stateless Service Kubernetes Autoscaling Min Replicas Value N The minimum number of replicas that will be started when Qorus is running under Kubernetes for stateless services only
Stateless Service Kubernetes Autoscaling Max Replicas Value N The maximum number of replicas that will be started when Qorus is running under Kubernetes for stateless services only
Stateless Service Kubernetes Autoscaling CPU Target Value N The percentage CPU usage goal for Kubernetes for all pods for stateless services only
Stateless Service Kubernetes Autoscaling Memory Target Value N The memory usage target for Kubernetes for stateless services only
Service Stateless Flag N The stateless flag for the service; indicates if the service is started externally to Qorus can can run in multiple processes
Service Modules Parameter N A list of Qore-language modules to be loaded into the service's Program container

Service Type

There are two types of services:

  • system: for system services delivered with Qorus
  • user: user services written / configured by Qorus developers

Service Name

The name of the service; the name and version together are unique identifiers for the service and are used to derive the serviceid (the single unique identifier for the service; it is generated from a database sequence when the service is loaded into the system via oload).

There can only be one service of a given type and name loaded in the system at any time, so the tyoe abd name of a service together also make a unique compound identifier for the job, however since system services are stable, and user services should avoid using names used by system services, the name of a service is often used as a unique key as well.

In case a user service might have the same name as a system service, the user service would be inaccessible with APIs (such as the REST API) where the service name is used as a unique key, therefore it is advised to never name a user service with the same name as a sytem service.

Service Version

Version string for the service; the version is informative; only the latest version of a service can be loaded in Qorus at any time, so services are referenced by name generally, and sometimes by type and name.

Service Patch

A string "patch" label which can be used to show that a service was updated while not affecting the serviceid.

Note
The patch value can be updated without affecting references to other objects; the unique ID for the object is not updated when the patch value is updated, however since service names are already treated as unique, the patch attribute is not as useful as in other objects but is still included in services for consistency's sake.

Service Description

Description of the service; accepts markdown for formatted output in the UI and IDE.

Service Author

The "author" value indicates the author of the service and will be returned with the service metadata in the REST API and also is displayed in the system UI.

Service Library

Services support library objects that provide additional code for the service.

See also
Library Objects for more information

Service Mappers

An optional list of mappers that are used in the service.

See also

Service Value Maps

An optional list of value maps that are used in the service.

See also

Service Language

The programming language used for the service implementation.

Service Source

The implementation of the service must be made in the programming language given by Service Language; the main job class must inherit one of the following classes depending on the source language: The source for a service is a class inheriting one of the following classes depending on the source language:

Service Constructors and Static Initialization

Service classes can have a constructor and classes can have static initialization, but please note that if the constructor or static class initialization requires features that are only available at runtime in Qorus itself, the errors raised in oload will cause service class instantiation or static class instantiation to fail.

To work around this, put all initialization requiring runtime support in Qorus in the init method of the service. This method is only called when the service is initialized by Qorus at runtime.

The service constructor must take no arguments.

Class-Based Service Static Class Variables

Static class variables are initialized when the class is loaded which is also performed by oload, therefore if any static class variables have initialization code that requires Qorus functionality, this can cause oload to fail, therefore it's recommended to put such static initialization in the class's constructor() method instead as in the preceding example.

Service Methods

Service methods define the user-visible interface of the service. The logic in a service program is defined by the method code and any library objects loaded into the service's program container.

Service methods are the user-visible entry-points to a service and as such contain the logic of the service.

The following diagram illustrates a subset of the attributes of a service method.

Service Method Metadata Diagram

Service Method Metadata

Key Mand.? Description
Service Method Name N The name of the method; also a public method with this name must be defined in the source as well
Service Method Description Y Description of the method object
Service Method Author N The optional author of the method, in case it differs from the service's author
Service Method Read Write Lock Setting N must be either none, read, or write (default none), to determine how the service's RWLock will be grabbed before the method is executed
Service Method Internal Flag N If this boolean flag is set to True, then the method will not be automatically exported through the any network interface
Service Method Write Flag N If this boolean flag is set to True, then the method will be marked as a write method, meaning that external callers will have to have the CALL-USER-SERVICES-RW role to call the method if RBAC security is enabled
Note
  • Both static and non-static class methods (or in the case of Python, class methods) can be exported as service methods.
  • Service method logic defined in methods of service classes can be implemented by Finite State Machines / Flow Designer instead; in case a method is defined with a Finite State Machine trigger for the method, the method logic defined in the service class will never be executed, and the method body should remain empty.

Service Method Name

The service method name must be unique (although service methods may be overloaded in Java and Qore) and is used when calling the method or accessing it using the REST API.

Service Method Author

A string giving the name of the author or authors of the service method.

Service Method Description

The description supports markdown in the UI and IDE.

Service Method Read Write Lock Setting

Each service has a read-write lock object (Qore class RWLock) associated with it. Methods can be tagged to acquire the lock for reading, writing, or not acquire the lock at all (default behavior).

This attribute can be used to provide safe multi-threaded access to internal resources. For example, if the service provides methods to read data and a method to reload the data from an external resource (such as a database), then the read methods can be tagged to acquire the lock in read mode, and the method to reload the data can be tagged to acquire the lock in write mode. This way, the reload method will block until all reads have finished, and the reads will block if any reload is in progress.

The read-write lock will be released safely when execution returns to the Qorus system, even if an exception is thrown in the method.

Note: Use the Read-Write Lock to Provide a Thread-Safe Caching Service
The Read-Write lock attribute on each method can be used to ensure consistent access to internal data, while allowing the data to be changed even while new requests come in during the update.

Service Method Internal Flag

If the "internal" flag is set to True on a method, then that method can only be called internally. Any direct call to this method from an external source (such as through lightweight web-service protocols exported through the HTTP server) will result in the method not being called and an exception returned to the called instead. This flag should be set on methods that return unserializable objects that cannot be sent through network interfaces.

Service Method Write Flag

If the "write" flag is set on a method, then users without the CALL-USER-SERVICES-RW (for user services) or the CALL-SYSTEM-SERVICES-RW (for system services) or another permission that contains these cannot call the service method from an external network interface.

Service Method Source

Depcrecated Java Service Method Annotations
When delivering compiled Java code implementing service methods in class or jar files, the Java annotation interface QorusMethod must be used to declare Qorus methods as in the following example:
class MyJavaService extends QorusService {
@QorusMethod(
// the desc value is required; all other values are optional
desc = "returns a constant string as an example",
// the author value is optional (default is empty)
author = "Josephine Programmer",
// this method is available from network interfaces by default (default)
intern = false,
// this method does not need write permissions to call (default)
write = false,
// no locking is done on the call (default)
lock = LockType.None
)
public static String getExampleString() {
return "hello";
}
}
const String

Note
  • Annotations are necessary to use when delivering Java code in binary form; i.e. as a class or in a jar file.
  • Annotations are not used when Java services are created using the Qorus IDE; in this case the method metadata is stored in YAML; see Implementing Qorus Objects Using YAML format for more information
  • Service method logic defined in methods of service classes can be implemented by Finite State Machines / Flow Designer instead; in case a method is defined with a Finite State Machine trigger for the method, the method logic defined in the service class will never be executed, and the method body should remain empty.
Restrictions on copy() Methods
To export a copy() service method with a Qore class-based service, name the class method _copy() (with a leading underscore), and give the method name copy(). The translation to the internal name (to avoid a conflict with the special class method copy()) is done automatically by oload and by the Qorus runtime. See the following example for more information.
Example Qore Class-Based Service with a Copy Method: copy-example-v1.0.qsd
%new-style
%strict-args
%require-types
%enable-all-warnings
class CopyExample inherits QorusService {
_copy() {
}
}
Disallowed Service Methods in Class-Based Services
The following special class methods may not be exported as service methods:
  • constructor()
  • destructor()
  • methodGate()
  • memberGate()
  • memberNotification()

Service Methods in Binary Java Files
Java service methods implemented in base classes in binary formats like jar or class files should be declared using the QoreMethod annotation.
Example Java Service Definition: MyJavaService.java
package com.qoretechnologies.qorus.example;
import qore.OMQ.UserApi.UserApi;
import qore.OMQ.UserApi.Service.*;
import java.time.ZonedDateTime;
class MyJavaService extends QorusService {
public static final ZonedDateTime systemStarted;
static {
try {
systemStarted = (ZonedDateTime)callRestApi("GET", "system/starttime");
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
// in order to be serve as a service init method, the method must be declared as a service method in the
// service's metadata
public void init() {
}
// in order to be serve as a service method, the method must be declared as a service method in the service's
// metadata
public static void other() {
}
}

Service Configuration Items

Services can declare configuration items in their YAML metadata to allow for the behavior of the service to be modified by auhtorized users at runtime using the operational web UI or the REST API.

Service configuration items are:

  1. Created by oload when loading the service
  2. Read at runtime with job APIs to affect the functionality of the service
  3. Updated using the web UI using the REST API with PUT /api/latest/services/{id_or_name}/config/{name}

Service configuration items are designed to allow users to affect the execution of a service so that changes can be made by authorized users in the UI without requiring a change to development.

Note
All configuration item values are considered to be managed by operations by design, which means that once a configuration item has been loaded into Qorus, any changes to its value with the API are persistent and will not be overwritten by subsequent loads of the configuration item by oload.

Strictly Local Service Config Item

If the strictly_local flag on a service configuration item is False, then the service configuration item is not local and the value can also be set on global level.

If the strictly_local flag is True, then service job configuration item is local and hence the value for this item cannot be set on global level.

Note
All configuration item values are considered to be managed by operations by design, which means that once a configuration item has been loaded into Qorus, any changes to its value with the API are persistent and will not be overwritten by subsequent loads of the configuration item by oload.

Reacting Dynamically to Service Config Item Changes

Services can react actively to configuration item changes by using the following APIs to register a callback that will be called when a config item value is updated:

Service File Resources

Service file resources are resource files that are declared with the "resource" tag in a service definition file and loaded into the database with oload.

These file resources can then be served automatically if the service binds a OMQ::AbstractServiceHttpHandler object with ServiceApi::bindHttp() to serve HTTP requests.

Note

Each resource name is its relative file name; for example if the following resource is declared in a service:

resource: html/* 

And the file html/extension.html is loaded by oload as a service file resource, then "html/extension.html" is a valid resource name, and could be set as the default resource (the resource that's served when no other resource is matched to a request) with AbstractServiceHttpHandler::setDefaultResource().

For example, if the code in the following examples is used to create the service HTTP handler object and bind the handler:

Python Example
from svc import QorusService
class MyHttpHandler(OMQ.AbstractServiceHttpHandler):
def __init__(self):
super(MyHttpHandler, self).__init__("my-service", None, None, None, False)
class MyService(QorusService):
def init(self):
svcapi.bindHttp(MyHttpHandler())
this class is used to customize and control the behavior of the Qorus HTTP server as it redirects URL...
Definition: AbstractServiceHttpHandler.qc:23
Qore Example
class MyHttpHandler inherits OMQ::AbstractServiceHttpHandler {
constructor() : OMQ::AbstractServiceHttpHandler("my-service", NOTHING, NOTHING, NOTHING, False) {
}
}
class MyService inherits QorusService {
init() {
bindHttp(new MyHttpHandler());
}
}
main Qorus namespace
Definition: QorusRbacAuth.qm:25

Then a request like the following is received:

GET /my-service/html/extension.html HTTP/1.1

The cx.resource_path value in the call to AbstractServiceHttpHandler::handleRequestImpl() will be set to "html/extension.html", which in this example would be a valid service file resource, therefore if the AbstractServiceHttpHandler::handleRequestImpl() returns NOTHING, this resource would be automatically served to the requestor.

Automatic Resource Template Rendering

Mixed text/HTML/JavaScript and Qore code can be automatically rendered and served if the resource has the file extension: "qhtml", "qjs", or "qjson". In this case the final rendered text is rendered and served automatically according to the rules described in WebUtil::StaticTemplateManager::add().

Summary of unrendered text format:

  • {% qore statement %}
  • {{ qore expression returning a string }}
Example HTML Template
{% foreach hash<auto> $h in ($ctx.list) { %}
<tr>
    <td class="name"><a href="?rpath={{ $ctx.parent_url ? $ctx.rpath : "" }}/{{ $h.name }}" {{ $h.type != "DIRECTORY" ? "class=\"file\"" : ""}}>{{ $h.name }}</a></td>
    <td class="text-left" width="40">{{ $h.type != "DIRECTORY" ? "File" : "Directory" }}</td>
    <td class="text-right">{{ $h.type != "DIRECTORY" ? $h.size : "--" }}</td>
</tr>
{% } %}
Example JavaScript Template
const ws_server = '{{"ws" + ($ctx.ssl ? "s" : "") + "://" + $ctx.hdr.host + "/" + UserApi::getConfigItemValue("websockets-root-uri")}}';

Service File Resource Template Programs

Qorus's service template manager sets up the embedded Program object for templates with additional code similar to a service Program.

The following are additions and enhancements available to template processing and rendering when used with Qorus services:

  • the following parse defines are set:
    • "Qorus" (common to workflows, services, and jobs)
    • "QorusServer" (common to workflows, services, and jobs)
    • "QorusHasUserConnections" (common to workflows, services, and jobs)
    • "QorusHasAlerts" (common to workflows, services, and jobs)
    • "QorusService" (service-specific)
    • "QorusHasHttpUserIndex" (service-specific)
    • all defines set in system option qorus.defines (common to workflows, services, and jobs)
  • the entire service api is available
  • service helper objects such as omqservice are also imported and available
  • the $ctx variable used in the template code has the value documented in OMQ::AbstractServiceHttpHandler::handleRequestImpl()
Note
  • The template program's parse options are set automatically by the parse-options attribute of the service; it's not possible to set them with parse directives inside the template, as injected code (which is injected as source) would be injected with the wrong style. This means that the standard Qore style in template programs is "old style"; i.e. variables require the dollar sign (ex: $ctx) by default; "new style" (without dollar signs) is only used if set in the service's metadata. As setting parse options is not currently supported with YAML-based service metadata, all service templates are parsed with old style for services with YAML-based metadata.

Service Autostart Flag

If the "autostart" flag is set to True, then the service will be started automatically whenever Qorus is started or whenever it and all interface groups it is a member of are enabled.

Note
The service "autostart" value is considered to be managed by operations, which means that once a service has been loaded into Qorus, if its "autostart" value is updated with the API, then those API-driven changes are persistent and will not be overwritten by subsequent loads of the service by oload.

Service Remote Flag

The "remote" flag indicates if the service will run as an independent qsvc process communicating with other elements of Qorus Integration Engine with a distributed queue protocol rather than internally in the qorus-core process.

When services run in separate qsvc processes, it provides a higher level of stability and control to the integration platform as a whole, as a service with implementation problems cannot cause the integration platform to fail.

There is a performance cost to running in separate qsvc processes; service startup and shutdown is slightly slower, and communication with qorus-core also suffers a performance hit as all communication must be serialized and transmitted over the network.

Calls to service methods in remote processes may not leave thread resources active when returning from the method; doing so will cause an exception to be thrown and for the thread resource to be cleaned up automatically.

Additionally, service methods running in remote qsvc processes cannot return non-serializable data (non-serializable objects, references, callable data types), and also their methods cannot accept these values as arguments. Furthermore, all HTTP or other protocol support implemented in remote services will be subject to round-trip network serialization to and from qorus-core.

Note
If your service has methods that require or return non-serializable data types, then the "remote" flag must be set to False.

The default for this option depends on the client option qorus-client.remote (if this client option is not set, then the default value is True).

The remote value can be changed at runtime by using the following REST API: PUT /api/latest/services/{id_or_name}?action=setRemote

Note
The remote flag is considered to be managed by operations, which means that once an interface has been loaded into Qorus, if its remote flag is updated with the API, then those API-driven changes are persistent and will not be overwritten by subsequent loads of the interface by oload.

Service Stateless Flag

"Stateless" services are services that scale horizontally across many processes and potentially many VM containers.

Statless services automatically support a "soft failover" for service calls and accesses to service resources; if a stateless service program instance dies in a call, then another client is chosen automatically and the request is sent to a running client without passing the error back to the caller.

Stateless services can serve HTTP, REST, FTP, and WebSocket requests; see the class documentation for the particular handler class for more information on how they behave with stateless services.

Note
The autostart flag is ignored for stateless services; stateless services are started and scaled automatically when running under Kubernetes; to stop a stateless services, the service must be disabled. See Stateless Service Support in Kubernetes for more information.

Stateless Service Support in Kubernetes

When Qorus is running under Kubernetes, stateful sets are created and destroyed automatically for stateless services, and additionally the associated pods are scaled to zero when stateless services (or an interface group that they are a member of) are disabled.

Qorus determines that it is running under Kubernetes if processes are started in independent mode and the KUBERNETES_SERVICE_HOST environment variable is set.

See also

Loading Stateless Services

Stateless services are always started externally to Qorus (for example by Kubernetes or OpenShift); they are assumed to always be remote, and additionally the autostart flag is ignored.

Unloading and Resetting Stateless Services

Stateless services are stopped when they are unloaded or reset; in such a case the external software can decide to restart them; normally they will be restarted when an unload or reset action is initiated by Qorus.

Stateless Service Kubernetes Initial CPU Requirements

The initial CPU requirement for stateless services when Qorus is running under Kubernetes expressed as a floating-point number (ex: 0.5 = half a CPU).

This number is used by the Kubernetes scheduler to determine if a service can be scheduled; if no node has enough free CPU resources, then the service will not be scheduled and cannot be started.

Stateless Service Kubernetes Initial Memory Requirements

The initial memory requirement for stateless services when Qorus is running under Kubernetes expressed as a string (ex: 250Mi or 2Gi).

This number is used by the Kubernetes scheduler to determine if a service can be scheduled; if no node has enough free memory, then the service will not be scheduled and cannot be started.

Stateless Service Kubernetes CPU Usage Limit

The hard CPU usage limit for stateless services when Qorus is running under Kubernetes expressed as a floating-point number (ex: 4.5 = four and a half CPUs). If the service exceeds this value, it will be subjected to CPU usage throttling.

Stateless Service Kubernetes Memory Limit

The hard memory limit for stateless services when Qorus is running under Kubernetes expressed as a string (ex: 250Mi or 2Gi).

If the service exceeds this value when running, it will be terminated.

Stateless Service Kubernetes Autoscaling Min Replicas Value

The minimum number of replicas that will be started when Qorus is running under Kubernetes.

This option is valid for stateless services only and also only takes effect when Qorus is running under Kubernetes.

Stateless Service Kubernetes Autoscaling Max Replicas Value

The maximum number of replicas that will be started when Qorus is running under Kubernetes.

This option is valid for stateless services only and also only takes effect when Qorus is running under Kubernetes.

Stateless Service Kubernetes Autoscaling CPU Target Value

The percentage CPU usage goal for Kubernetes for all pods for the service when Qorus is running under Kubernetes.

This option is valid for stateless services only and also only takes effect when Qorus is running under Kubernetes.

Stateless Service Kubernetes Autoscaling Memory Target Value

The memory usage target for pods for the stateless service when Qorus is running under Kubernetes.

This option is valid for stateless services only and also only takes effect when Qorus is running under Kubernetes.

Service Modules Parameter

The "service-modules" option lists modules providing base classes for Qore-language services classes.

Modules declared like this will be loaded into each service's Program container, and their classes can be used as base classes for service classes.

See also
Qore-Language Service Extension Modules for more information.

Service Loading

Otherwise, a service is loaded and initialized any time a method of that service is called and the service has not already been loaded. In this case all the service's methods are loaded and parsed into the same program object, along with any library function, classes, and constants associated with the service. Any parse exceptions during service loading will prohibit the service from being loaded.

Service Initialization

If a service has an init() method, this method is called as soon as the service has been loaded and parsed, and before any call to a service method is made. For example, if a service with an init() method is loaded because a call to another method is made, the init() method will be called first, and then the called method will be called and the value returned to the caller.

An exception when running the init() method will cause the service to be unloaded, and the exception will be returned to the caller.

If the service has a start() method, then it must also have a stop() method. The start() method will be run in a separate thread and is expected to run until the stop() method is called. The stop() method should signal the routine running in the start() method to exit.

When the start() method returns, the service is automatically unloaded. To start threads in user code in a service, use the svc_start_thread() or svc_start_thread_args() functions (in which case, to use these functions, the service must also have a stop() method).

Note
When the init() method is explicitly called, this call is ignored, in the sense that, if the service is already loaded, no action is taken, and if it is not, the service is loaded and initialized, so the init() method is called as per the rules above. Therefore to simple ensure that a service is loaded and initialized, it is safe to call the init() method of a service at any time. Also note that the init() method may be safely called even if no init() method is defined for the service.

Service HTTP Handlers

Qorus services can implement generic HTTP request handlers by subclassing OMQ::AbstractServiceHttpHandler and binding it either to the global Qorus listeners or service-specific listeners using:

Using this mechanism, Qorus services can implement custom HTTP-based protocols or use one of the existing heler classes to export RPC services, REST APIs, web sockets server event sources, or even generic HTML user interfaces and Qorus web UI extensions.

Developing Web UI Extensions

The web UI can be extended with Qorus services by registering the extension with the following function:

The only argument of this function must be a class descended from OMQ::QorusExtensionHandler as in the following examples:

Python Example
from svc import QorusService
class myExtensionHandler(OMQ.QorusExtensionHandler):
def __init__(self):
super(myExtensionHandler, self).__init__("Example", "Example Extension", "my example extension")
self.setDefaultResource("templates/index.qhtml")
class MyService(QorusService):
def init(self):
svcapi.uiExtensionRegister(myExtensionHandler())
this class is used to define a Qorus UI extension in a Qorus service
Definition: AbstractServiceHttpHandler.qc:501
Qore Example
%new-style
%strict-args
%require-types
%enable-all-warnings
class myExtensionHandler inherits QorusExtensionHandler {
constructor() : QorusExtensionHandler("Example", "Example Extension", "my example extension") {
setDefaultResource("templates/index.qhtml");
}
}
class MyService inherits QorusService {
init() {
uiExtensionRegister(new myExtensionHandler());
}
}

UI Extension Resources

At least one service file resource is required: the bootstrap HTML page; this resource is set by calling OMQ::QorusExtensionHandler::setDefaultResource() as in the above example.

Resources must be declared in the service definition file with a service file resource declaration.

The following is an example UI extension service with example resources.

index.html
<h1>Hello World!</h1>
Resource Tag
resource: index.html 
Python Example (hello-world-v1.0.py)
from service import QorusService
class HelloWorldHandler(OMQ.QorusExtensionHandler):
def __init__(self):
super(myExtensionHandler, self).__init__("examples", "Hello World")
self.setDefaultResource("index.html")
class HelloWorld(QorusService):
def init(self):
svcapi.uiExtensionRegister(HelloWorldHandler())
Qore Example (hello-world-v1.0.qsd)
%new-style
%strict-args
%require-types
%enable-all-warnings
class HelloWorldHandler inherits QorusExtensionHandler {
constructor(): QorusExtensionHandler("examples", "Hello World") {
setDefaultResource('index.html');
}
}
class HelloWorld inherits QorusService {
# "init" is a registered service method in the corresponding YAML metadata file
init() {
uiExtensionRegister(new HelloWorldHandler());
}
}

Service REST APIs

REST APIs can be developed with Qorus services by subclassing the OMQ::AbstractServiceRestHandler class and bound with:

Consider the following examples:

Python Example (rest-example-v1.0.py)
from svc import QorusService
from qore.RestHandler import RestHandler, AbstractRestClass
class ExampleRestClass(AbstractRestClass):
def name(self):
return "subclass"
def get(self, cx, ah):
return RestHandler.makeResponse(200, "OK")
class ExampleRestHandler(OMQ.AbstractServiceRestHandler):
def __init__(self):
super(MyRestHandler, self).__init__("example")
self.addClass(ExampleRestClass())
def get(self, cx, ah):
return RestHandler.makeResponse(200, "OK");
class PythonRestTest(QorusService):
def init(self):
svcapi.bindHttp(ExampleRestHandler())
This class is used to customize and control the behavior of the Qorus HTTP server.
Definition: AbstractServiceHttpHandler.qc:626
Qore Example (rest-example-v1.0.qsd)
%new-style
%strict-args
%require-types
%enable-all-warnings
# example class for handling REST operations
class ExampleRestClass inherits RestHandler::AbstractRestClass {
string name() {
return "subclass";
}
hash get(hash cx, *hash ah) {
return RestHandler::makeResponse(200, "OK");
}
}
class ExampleRestHandler inherits AbstractServiceRestHandler {
constructor() : AbstractServiceRestHandler("example") {
addClass(new ExampleRestClass());
}
hash get(hash cx, *hash ah) {
return RestHandler::makeResponse(200, "OK");
}
}
class RestExample inherits QorusService {
# "init" is a registered service method in the corresponding YAML metadata file
init() {
bindHttp(new ExampleRestHandler());
}
}

This is a simple service declaration that exports a (very simple) REST API at "/example" that returns the string "OK" to the following requests:

  • GET /example HTTP/1.1: handled by ExampleRestHandler
  • GET /example/subclass HTTP/1.1: handled by ExampleRestClass

Service WebSockets

Qorus services can provide websocket server services by subclassing OMQ::AbstractServiceWebSocketHandler and bound with:

Consider the following examples:

Python Example (websocket-example-v1.0.qsd.py)
from svc import QorusService
from qore.WebSocketHandler import WebSocketHandler, WebSocketConnection
from qore.json import make_json
from datetime import timedelta
quit = False
ExitPollInterval = timedelta(seconds = 1)
class MyWebSocketConnection(WebSocketConnection):
def __init__(self, handler):
super(MyWebSocketConnection, self).__init__(handler)
def gotMessage(self, msg):
UserApi.logInfo("got msg: %y", msg)
class MyWebSocketHandler(OMQ.AbstractServiceWebSocketHandler):
def __init__(self):
super(MyWebSocketHandler, self).__init__("my-websocket")
svcapi.startThread(self.eventListener)
def eventListener(self):
global quit
global ExitPollInterval
min_id = 1
while not quit:
eh = svcapi.waitForEvents(min_id, ExitPollInterval)
if 'shutdown' in eh:
UserApi.logInfo("system is shutting down; stopping event thread")
break
min_id = eh['lastid'] + 1
if not 'events' in eh:
continue
json = make_json(eh['events'])
self.sendAll(json)
def getConnectionImpl(self, cx, hdr, cid):
return MyWebSocketConnection(self)
class PythonWebSocketTest(QorusService):
# "init" is a registered service method in the corresponding YAML metadata file
def init(self):
svcapi.bindHttp(MyWebSocketHandler())
# "stop" is a registered service method in the corresponding YAML metadata file
def stop(self):
global quit
quit = True
this class is used to allow Qorus services to provide dedicated WebSocket server services to external...
Definition: AbstractServiceHttpHandler.qc:827
string make_json(any data, *int format, *string encoding)
Qore Example (websocket-example-v1.0.qsd)
%new-style
%strict-args
%require-types
%enable-all-warnings
class MyWebSocketHandler inherits OMQ::AbstractServiceWebSocketHandler {
public {
const ExitPollInterval = 1s;
}
constructor() : AbstractServiceWebSocketHandler("my-websocket") {
ServiceApi::startThread(\eventListener());
}
private eventListener() {
int min_id = 1;
while (!quit) {
hash eh = ServiceApi::waitForEvents(min_id, ExitPollInterval);
if (eh.shutdown) {
UserApi::logInfo("system is shutting down; stopping event thread");
break;
}
min_id = eh.lastid + 1;
if (!eh.events)
continue;
sendAll(make_json(eh.events));
}
}
}
class WebSocketExample inherits QorusService {
# "init" is a registered service method in the corresponding YAML metadata file
init() {
our *bool quit;
bindHttp(new MyWebSocketHandler());
}
# "stop" is a registered service method in the corresponding YAML metadata file
stop() {
quit = True;
}
}

The above example service will listen for all system events with waitForEvents() (polling every second for a service stop event) and serialize any new system events with JSON using make_json() and send to all connected clients using WebSocketHandler::sendAll().

Service FTP Handlers

Qorus services can implement a custom FTP server for receiving data by subclassing OMQ::AbstractFtpHandler and calling the following API on the resulting object:

Consider the following example services:

Python Example (python-ftp-example-v1.0.qsd.py)
from svc import QorusService
from OMQ import AbstractFtpHandler
import os
from os.path import dirname, basename, exists
# import symbols from Qore modules
from qore.Util import get_random_string
from qore.FsUtil import remove_tree
# import symbol from Qore
from qore.__root__.Qore import mkdir_ex
dir = ""
rmdir = False
stats = {
"in": {
"files": 0,
}
}
class MyFtpHandler(AbstractFtpHandler):
def __init__(self, dir):
super(MyFtpHandler, self).__init__(dir)
def authReceiveFile(self, cwd, orig, path):
path += ".tmp"
UserApi.logInfo("authorizing receipt of file %s -> %s", path, path)
return path
def fileReceived(self, path):
global stats
stats['in']['files'] += 1
UserApi.logInfo("received file: dir: %y fn: %y", dirname(path), basename(path))
class PythonFtpTest(QorusService):
# "init" is a registered service method in the corresponding YAML metadata file
def init(self):
global dir
dir = os.environ['OMQ_DIR'] + "/user/ftp-test" + get_random_string()
if not exists(dir):
global rmdir
mkdir_ex(dir, 0o700, True)
rmdir = True
handler = MyFtpHandler(dir)
handler.addListener(0)
svcapi.bindFtp(handler)
# "info" is a registered service method in the corresponding YAML metadata file
def info(self):
global stats
return stats
# "stop" is a registered service method in the corresponding YAML metadata file
def stop(self):
global dir
global rmdir
if rmdir:
svcapi.logInfo("removing FTP tree: %y", dir)
remove_tree(dir)
# "port" is a registered service method in the corresponding YAML metadata file
def port(self):
res = svcapi.getServiceInfo()['resources']
for key in (res):
if res[key]['type'] == "FtpListener" and res[key]['info']['familystr'] == "ipv4":
return res[key]['info']['port']
raise QoreException("FTP-ERROR", "cannot determine FTP port")
bool exists(...)
Qore Example (ftp-example-v1.0.qsd)
%new-style
%strict-args
%require-types
%enable-all-warnings
const DEFAULTS = {
"listeners": list(18002),
"dir": "/tmp",
};
class MyFtpHandler inherits public OMQ::AbstractFtpHandler {
constructor(string dir) : AbstractFtpHandler(dir) {
}
*string authReceiveFile(string cwd, string orig, reference path) {
UserApi::logInfo("authorizing receipt of file %s -> %s", path, path + ".tmp");
path += ".tmp";
}
fileReceived(string path) {
++stats."in".files;
UserApi::logInfo("received file: dir: %y fn: %y", dirname(path), basename(path));
}
}
class FtpExample inherits QorusService {
private {
hash<auto> stats = {
"in": {
"files": 0,
},
};
}
# "init" is a registered service method in the corresponding YAML metadata file
init() {
hash<auto> opt = getOptionsSetDefaults();
logInfo("starting with options: %y", opt);
MyFtpHandler handler(opt.dir);
foreach auto l in (opt.listeners) {
handler.addListener(l);
}
bindFtp(handler);
}
# "info" is a registered service method in the corresponding YAML metadata file
hash<auto> info() {
return stats;
}
private hash getOptionsSetDefaults() {
# get persistent options for service from system properties
*hash<auto> props = propGet("ftp-example");
# allow system properties to override default values
return map {$1.key: props{$1.key} ?? $1.value}, DEFAULTS.pairIterator();
}
}
list< auto > list(...)

This service will create an FTP server on port 18002 by default and store any files received in the "/tmp" directory by adding ".tmp" on the filename.

The AbstractFtpHandler::fileReceived() callback could then be modified to create a workflow order instance for the file's data if necessary (for example).