Java, .NET and native code

A Java jar, a .NET assembly or a native binary run as a supervised helper process.

When the client for a system is a Java jar the vendor ships, a .NET assembly or a native binary, start_process runs it as a helper process next to the server. The Connector SDK starts the process with the connection, supervises it, restarts the connection if the process dies unexpectedly, and stops the process when the definition is deleted. Services use the connection like any other.

The connector module

The example 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 talks to 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)

        # The JVM needs a moment before it 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}')

In the code above:

  • start_process is provided by the platform, never overridden - it allocates a local port, substitutes it for any {port} placeholder in the command and starts the process
  • The returned object has four attributes - pid, port, is_running and stop
  • Supervision means the platform watches the process - if it dies unexpectedly, the whole connection is rebuilt with backoff, which re-runs create_client and so starts a new helper
  • The helper stops with the connection - deleting or editing the definition stops the process too
  • A .NET assembly or a native binary works exactly the same way - only the command changes

Creating a definition

Definitions are managed with enmasse under a key derived from the connector's type - custom_ plus the type name:

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

Learn more