The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

RPC::Switch::Client::Tiny - Lightweight client for the RPC-Switch.

SYNOPSIS

  use RPC::Switch::Client::Tiny;

  # init rpcswitch client
  my $s = IO::Socket::SSL->new(PeerAddr => $host, Proto => 'tcp', Timeout => 30,
        SSL_cert_file => $cert_file, SSL_key_file => $key_file,
        SSL_verify_mode => SSL_VERIFY_PEER, SSL_ca_file => $ca_file);

  sub trace_cb {
        my ($type, $msg) = @_;
        printf "%s: %s\n", $type, to_json($msg, {pretty => 0, canonical => 1});
  }

  my $client = RPC::Switch::Client::Tiny->new(sock => $s, who => $who, timeout => 60, trace_cb => \&trace_cb);

  # call rpcswitch worker
  my $res = $client->call('test.ping', {val => 'test'}, $options);

  # run rpcswitch worker
  sub ping_handler {
        my ($params, $rpcswitch) = @_;
        return {success => 1, msg => "pong $params->{val}"};
  }

  my $methods = {
        'test.ping' => {cb => \&ping_handler, doc => $doc},
        'test.tick' => {cb => \&tick_handler}
  }
  $client->work($workername, $methods, $options);

DESCRIPTION

RPC::Switch::Client::Tiny is a lightweight RPC-Switch client.

This module works on a single socket connection, and has no dependencies on the Mojo framework like the RPC::Switch::Client module.

The rpctiny tool included in the examples directory shows how to configure and call a worker handler using a local installation of the rpc-switch server.

References

The implementation is based on the following protocols:

- json-rpc 2.0: https://www.jsonrpc.org/specification

- netstring proto: http://cr.yp.to/proto/netstrings.txt

- rpc-switch: https://github.com/a6502/rpc-switch

Error Handling

The RPC::Switch::Client::Tiny::Error always contains a 'type' and a 'message' field. It might contain additional fields depending on the error type:

  my $res = eval { $client->call($method, $params, $options) };
  if (my $err = $@) { 
      die "client $err->{type} error[$err->{code}]: $err->{message}" if $err->{type} eq 'jsonrpc';
      die "client $err->{type} error[$err->{class}]: $err->{message}" if $err->{type} eq 'worker';
      die "client $err");
  }

  type 'jsonrpc': 'code':  jsonprc error returned by the rpcswitch 
  type 'worker':  'class': hard/soft
                  'name':  somthing like BACKEND_ERROR
                  'text':  detailed error message
                  'from':  sinding module
                  'data':  additional error information

Encoding

All methods expect and return strings in utf8-format, when the option client_encoding_utf8 is enabled.

To pass latin1-strings as parameter, a caller would have to convert the input first. (see: $utf8 = Encode::encode('utf8', $latin1)).

The json-rpc messages are also utf8-encoded when they are transmitted. (see: https://metacpan.org/pod/JSON#utf8)

Without the client_encoding_utf8 option, all passed strings are utf8-encoded for the json-rpc transmission, because the client character-encoding is assumed to be unknown. This might transmit doubly utf8-encoded strings.

Async handling

When $client->work() is called with a value of max_async > 0, then the rpc_handler will fork for each request from a caller and use use pipes for the synchronization with the childs.

This works like this:

  1) parent opens a pipe and sends RES_WAIT after the child forked.
  2) the child calls the worker-handler and writes RES_OK/RES_ERROR to the pipe.
  3) the parent rpc_handler() reads from the pipe and forwards the
     message to the rpcswitch caller. It closes the pipe afterwards.
  4) if the child dies or no valid result can be read from the pipe,
     the parent sends a RES_ERROR message to the rpcswitch caller. 

Session Cache

When session handling is enabled a child might process more than one request with the same session_id.

The session handling is loosely based on the HTTP Set-Cookie/Cookie Mechanism described in RFC 6265, and uses json paramters instead of http-headers. (see: https://datatracker.ietf.org/doc/html/rfc6265)

- the worker signals a set_session request via:

  $params = {.., set_session => {id => $id, expires => $iso8601}}

- the caller signals a session request via:

  $params = {.., session => {id => $id}}

Just one active child is used for each session_id, and the child can handle just one request at a time. If the child for a session is busy, a new child without session support will be used for the request.

Caller Retry Handling

A caller should be able to distinguish between hard errors and errors where a later retry is possible because the service is currently not available. These are for example:

  1) Socket connect-timeouts because the rpcswitch or the
     network is not available.
     (socket connect $@: Connection refused)
  2) The rpcswitch socket closes.
     (io error: eof)
  3) No Worker is currently available for method.
     (jsonrpc error -32003: No worker available for $method)
  4) The Worker terminates for an active request (got RES_WAIT)
     (rpcswitch error: rpcswitch.channel_gone for request)
  5) The Worker terminates for a queued request
     (jsonrpc error -32006: opposite end of channel gone)
  6) The callers Request Timeout expires
     (jsonrpc error: receive timeout)

If more than one worker is running, and only a single worker terminates, it could be useful to retry operations immediately.

TODO: consolidate error codes to allow better match on 'timeout'?

Flow Control

Since the communication with the rpcswitch uses a single socket, the socket can't be used for flowcontrol. All incoming worker requests have to be queued, so that the socket is available for other protocol messages.

In normal operation the worker jobqueue could grow to a very large size, if clients add new requests faster than they can be handled.

If $client->{work} is called with the {flowcontrol => 1} option, the worker will withdraw its methods when it runs in async mode and the jobqueue reaches the hiwater mark. When the jobqueue size falls below the lowater mark, all methods will be announced again.

The client will see the 'channel gone' messages (4) and (5) if a method is withdrawn because of flowcontrol , and can retry the operation later.

METHODS

new

  $client = RPC::Switch::Client::Tiny->new(sock => $s, who => $w, %args);

The new contructor returns a RPC::Switch::Client::Tiny object. Arguments are passed in key => value pairs.

The only required arguments are `sock` and `who`.

The client is responsible to pass a connected socket to ->new() and to close the socket after all client-calls are complete.

The new constructor dies when arguments are invalid, or if required arguments are missing.

The accepted arguments are:

sock: connected socket (error if nonblocking) (required)
who: rpcswitch client name (required)
auth_method: optional (defaults to 'password' or 'clientcert')
token: token for auth_method 'password' (optional for 'clientcert')
client_encoding_utf8: all client strings are in utf8-format
timeout: optional rpc-request timeout (defaults to 0 (unlimited))
trace_cb: optional handler to log SND/RCV output

The trace_cb handler is called with a message 'type' indicating the direction of a message, and with a 'msg' object containing the decoded 'json-rpc' message:

  sub trace_cb {
        my ($type, $msg) = @_;
        printf "%s: %s\n", $type, to_json($msg, {pretty => 0, canonical => 1});
  }

call

  $res = $client->call($method, $params, $options);

Calls a method of a rpcswitch worker and waits for the result.

On success the result from a rpcswitch RES_OK response is returned.

On failure the method dies with a RPC::Switch::Client::Tiny::Error. When the method call is trapped with eval, the error object is returned in '$@'.

The arguments are:

method: worker method to call like: 'test.ping'
params: request params like: {val => 'test'}
options->{reqauth}: optional rpcswitch request authentication
options->{timeout}: optional per call timeout

work

  $client->work($workername, $methods, $options);

Runs the passed rpcswitch worker methods.

Dies with RPC::Switch::Client::Tiny::Error on failure.

Returns without die() only when the remote side cleanly closed the connection, and there are no outstanding requests ($@ == '' for eval).

workername: name of the worker
methods: worker methods to announce
options->{max_async}: run multiple forked workers using async RES_WAIT notification
options->{flowcontrol}: announce/withdraw async methods based on load
options->{max_session}: enable session cache for async worker childs
options->{session_expire}: default session expire time in seconds
options->{session_idle}: session idle time in seconds for user session updates
options->{session_persist_user}: reuse active user sessions for given user param
options->{max_user_session}: limit session cache per optional user field

The options are described in their respective sections unter DESCRIPTION.

The methods parameter defines the worker methods, which are announced to the rpcswitch. Each method is passed as a 'method_name => $method_definition' tupel.

The valid fields of the $method_definition are:

cb: method handler (required)
doc: optional documentation for method
filter: optional filter to restrict method subset of params

The provided handler is called when the method is called via the rpcswitch.

  sub ping_handler {
        my ($params, $rpcswitch) = @_;
        return {success => 1, msg => "pong $params->{val}"};
  }

  my $methods = {
        'test.ping' => {cb => \&ping_handler, doc => $doc},
        'test.tick' => {cb => \&tick_handler}
  }

The optional documentation provided to the rpcswitch can be retrieved by calling the rpcswitch.get_method_details method. The format of the documentation field is:

  doc => {
        description => 'send a ping', inputs => 'val' outputs => 'msg',
  }

The optional filter expression allows a worker to specify that it can process the method only for a certain subset of the method parameters. For example the filter expression {'host' => 'example.com'} would mean that the worker can only handle method calls with a matching params field.

Filter expressions are limited to simple equality tests on one or more keys. These keys have to be configured in in the rpcswitch action definition, and can be allowed, mandatory or forbidden per action.

SEE ALSO

Used submodules for Error object RPC::Switch::Client::Tiny::Error, Netstring messages RPC::Switch::Client::Tiny::Netstring, Async child handling RPC::Switch::Client::Tiny::Async and Session cache RPC::Switch::Client::Tiny::SessionCache.

JSON encoder/decoder JSON

AUTHORS

Barnim Dzwillo @ Strato AG

COPYRIGHT AND LICENSE

Copyright (C) 2022 by Barnim Dzwillo

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.