I finally managed to solve the problem in Python. It's not perfect but it works:

Class User.Timer Extends %RegisteredObject
{

Property Executor [ Private ];

Method Initialize(maxWorkers As %Integer = 4) [ Language = python ]
{
import concurrent.futures
import time
import threading

self.Executor = concurrent.futures.ThreadPoolExecutor(max_workers=maxWorkers)
}

Method Close() [ Language = python ]
{
if self.Executor:
    self.Executor.shutdown()
}

Method Greet(name)
{
    Write "Hello ", name, !
}

Method OnCallback0(methodName As %String) [ Private ]
{
    Do $METHOD(instance, methodName)
}

Method OnCallback1(instance As %RegisteredObject, method As %String, arg1) [ Private ]
{
    Do $METHOD(instance, method, arg1)
}

Method OnCallback2(instance As %RegisteredObject, method As %String, arg1, arg2) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2)
}

Method OnCallback3(instance As %RegisteredObject, method As %String, arg1, arg2, arg3) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3)
}

Method OnCallback4(instance As %RegisteredObject, method As %String, arg1, arg2, arg3, arg4) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3, arg4)
}

Method OnCallback5(instance As %RegisteredObject, method As %String, arg1, arg2, arg3, arg4, arg5) [ Private ]
{
    Do $METHOD(instance, method, arg1, arg2, arg3, arg4, arg5)
}

Method InternalRun(delayMs As %Integer, wait As %Boolean, instance As %RegisteredObject, method As %String, args... As %List) [ Internal, Language = python ]
{
import time
import iris

if not self.Executor:
    raise Exception("The 'Initialize' method has not been called.")

def worker_function():
    time.sleep(delayMs / 1000)
    if len(args) == 0:
        self.OnCallback0(instance, method)
    elif len(args) == 1:
        self.OnCallback1(instance, method, args[0])
    elif len(args) == 2:
        self.OnCallback2(instance, method, args[0], args[1])
    elif len(args) == 3:
        self.OnCallback3(instance, method, args[0], args[1], args[2])
    elif len(args) == 4:
        self.OnCallback4(instance, method, args[0], args[1], args[2], args[3])
    elif len(args) == 5:
        self.OnCallback5(instance, method, args[0], args[1], args[2], args[3], args[4])
    else:
        raise Exception("Too many arguments.")
    return 0


future = self.Executor.submit(worker_function)

# wait == 0 means fire-and-forget
try:
    if (wait == 1):
        rv = future.result()

except Exception as e:
    print(f"{e}")
}

/// delayMs  - the parameter specifies the timer delay in milliseconds
/// wait     - if the parameter is false, the process will not wait for the Future result to be returned (fire-and-forget)
/// instance - any object which method should be called with a delay
/// method   - specifies the object's callback method name
/// args     - the callback method arguments (up to 5)
Method Run(delayMs As %Integer, wait As %Boolean, instance As %RegisteredObject, method As %String, args... As %List)
{
    Do ..InternalRun(delayMs, wait, instance, method, args...)
}

ClassMethod Test()
{
    Set obj = ##class(Timer).%New()
    Do obj.Initialize()
    Do obj.Run(1000, 0, obj, "Greet", "John")
    Do obj.Run(2000, 0, obj, "Greet", "Jessica")
    Write "If 'wait == 0' this line will be printed first", !
    Do obj.Close()
}

}

Hi Timo,

Yesterday I experimented a lot with both Python and workers (and failed).

Here is an example. In this code I want the workers to be started as background tasks, so the program should first print "This should be printed first", then each worker (each started with random delay interval) shoud print its own message. This obviously doesn't happen because of the queue.Sync() call but I don't want to wait for all the workers to complete.

Class DelayedTest Extends %RegisteredObject
{

ClassMethod Callback(interval As %String) As %Status
{
    Hang interval
    Write "Interval = ", interval, !
    Return $$$OK
}

Method RunWorkers()
{
    #Dim queue as %SYSTEM.WorkMgr
    Set queue = ##class(%SYSTEM.WorkMgr).%New()
    For i = 1:1:5
    {
        Set status = queue.Queue("..Callback", $RANDOM(5) + 1) // Minimal delay is 1 second
        $$$ThrowOnError(status)
    }

    Set status = queue.Sync()
    $$$ThrowOnError(status)
}

ClassMethod Main()
{
    #Dim d = ##class(DelayedTest).%New()
    Do d.RunWorkers() 
    Write "This should be printed first" 
}

}

Also, I'm not sure that Hang is appropriate here to emulate some delay. If it works like Sleep it should block the main thread.

Here it is (implemented as Mixin):

Class MyNamespace.Pooled Extends Ens.Host [ Abstract ]
{

Property PoolIndex As %Integer [ Calculated ];

Method PoolIndexGet() As %Integer
{
    #Dim cn as %String
    Set cn = ..%ConfigName  

    #Dim statement as %SQL.Statement
    Set statement = ##class(%SQL.Statement).%New()
    Set status = statement.%PrepareClassQuery("Ens.Job","Enumerate")
    $$$ThrowOnError(status)

    #Dim rs as %SQL.StatementResult
    Set rs = statement.%Execute()
    #Dim i as %Integer = -1

    While (rs.%Next()) 
    {
        #Dim jobId as %String 
        Set jobId = rs.%Get("Job")
        If (rs.%Get("ConfigName") = cn)
        {
            Set i = i + 1
            If (jobId = $JOB) 
            {
                Kill rs
                Return i
            }
        }
    }
    Kill rs
    Return i
}

Property PoolSize As %Integer [Calculated];

Method PoolSizeGet() As %Integer
{
    #Dim cn as %String
    Set cn = ..%ConfigName
    #Dim statement as %SQL.Statement
    Set statement = ##class(%SQL.Statement).%New()
    Set status = statement.%PrepareClassQuery("Ens.Job","Enumerate")
    $$$ThrowOnError(status)

    #Dim rs as %SQL.StatementResult
    Set rs = statement.%Execute()
    #Dim i as %Integer = 0

    While (rs.%Next()) 
    {
        If (rs.%Get("ConfigName") = cn)
        {
            Set i = i + 1
        }
    }
    Kill rs
    Return i
}

}

I faced similar issues with Postgres linked tables because of different SQL syntax and also poorly implemented translation from IRIS dialect to your linked server dialect. The workaround is to add an instance of EnsLib.SQL.Operation.GenericOperation to your production and to execute SQL queries via ODBC/JDBC bypassing IRIS, something like:

Set operation = ##class(EnsLib.SQL.Operation.GenericOperation).%New("NameOfYourProductionComponent")
#Dim rs as EnsLib.SQL.GatewayResultSet
Set status = operation.Adapter.ExecuteQuery(.rs, "select 1", .args)
While (rs.Next()) {
   ...
}
Do rs.Close()

To be honest... no :) Actually I think I badly need a standalone terminal (but Studio is discontinued) or a Python/Java/C#/whatever library which is able to invoke arbitrary commands on a remote server and I'm just wondering if such libraries exist.

PS leveraging WebTerminal API looks attractive but it seems to me it doesn't expose Swagger/OpenAPI definitions

Great, thanks a lot, it works.

The real error messages are "ERROR #6162: Unable to create HTTP Authorization header for NTLM scheme." and "ERROR #6162: Unable to create HTTP Authorization header for Negotiate scheme." for both NTLM and Negotiate schemes respectively.

Does it mean that IRIS is unable to deal with these authentication schemes? The documentation says that NTLM is supported.