Java, .NET and native code

Call Java, .NET or native clients from services as named connections.

When the client for a system is a Java jar the vendor ships, a .NET assembly or a native binary, start_process from the Connector SDK runs it as a helper process next to the server. The process starts and stops with the connection, the platform supervises it and services use the connection like any other.

The connector module

The connector wraps a Java inventory component - a jar that serves requests over a local socket:

# -*- coding: utf-8 -*-

# stdlib
import socket
import time

# Zato
from zato.common.sdk import Connector, Field

# How long to wait for the helper process to start accepting connections, in seconds.
_startup_timeout = 15

class InventoryClient:
    """ A client for the inventory system, which is a Java component - the connector runs its jar
    as a helper process and this client calls it over a local socket, one connection per request.
    """
    def __init__(self, host:'str', port:'int') -> 'None':
        self.host = host
        self.port = port

    def send(self, data:'str') -> 'str':

        # Connect for the duration of one request ..
        with socket.create_connection((self.host, self.port)) as conn:
            conn.sendall(f'{data}\n'.encode('utf8'))

            # .. and read the response line back.
            with conn.makefile('r', encoding='utf8') as reader:
                response = reader.readline()

        return response.strip()

class InventoryConnector(Connector):
    """ Wraps a Java inventory component - a jar run and supervised as a helper process
    that services access through self.out.inventory.
    """
    type = 'inventory'

    # Configuration schema
    jar_path = Field.Text()

    def create_client(self) -> 'InventoryClient':

        # Run the jar as a supervised helper process - the '{port}' placeholder holds
        # the local port allocated for it.
        process = self.start_process(['java', '-jar', self.config.jar_path, '{port}'])

        client = InventoryClient('127.0.0.1', process.port)

        # Wait until the JVM accepts connections.
        deadline = time.monotonic() + _startup_timeout

        while True:
            try:
                client.send('hello')
            except OSError:
                if time.monotonic() > deadline:
                    raise Exception(f'The inventory helper did not start within {_startup_timeout}s')
                time.sleep(0.2)
            else:
                break

        self.logger.info('Inventory helper started for `%s` (pid %s)', self.name, process.pid)
        return client

    def ping(self, client:'InventoryClient') -> 'None':
        client.send('ping')

    def get_stock(self, item:'str') -> 'str':
        return self.client.send(f'stock {item}')

The platform provides start_process - it allocates a local port, substitutes it for the {port} placeholder in the command and starts the process. The returned object has four attributes - pid, port, is_running and stop.

The platform supervises the process. When the process exits unexpectedly, the platform rebuilds the whole connection with backoff, which re-runs create_client and starts a new helper. Deleting or editing the definition stops the process together with the connection.

A .NET assembly or a native binary works the same way - only the command changes.

Create a definition

Create the definition in an enmasse file, under the custom_inventory key - custom_ followed by the connector's type:

custom_inventory:
  - name: My Inventory
    jar_path: /opt/vendor/inventory-client.jar

The jar runs and stops with the connection and services call it like any other connection type.

See also

PageWhat it covers
SDK referenceField types, lifecycle methods and the behavior of every invocation
Command-line toolsOne-shot commands and long-lived tools like ngrok as connections
Python libraries in a separate interpreterRun a Python library in an interpreter of its own and call it from services