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.