Harp daemon architecture

TODO

Internal modules

Network server and daemonization operations

class harpd.daemon.SSLServer(host, port, procs, authdb, cert_file, key_file, ca_file=None)
Parameters:
  • host (string or None) – address to bind to
  • port (integer) – port to listen on
  • procs (dict(name->callable)) – table with procedures to expose
  • cert_file (path) – X.509 certificate
  • key_file (path) – private key for cert_file
  • ca_file (path) – file with all X.509 CA certificates

SSL connection server. Uses RequestHandler to handle SSL connections.

close_request(client_socket)
Parameters:client_socket – socket to close

Close the client socket, but without tearing down the connection (e.g. SSL shutdown handshake).

In forking SocketServer, this is called in parent (listener) process, and may be called by implementation of shutdown_request().

fileno()
Returns:file descriptor

Return file descriptor to wait for I/O events (poll()).

finish_request(*args, **kwargs)

Main work method. Creates a RequestHandler instance and does all the processing.

This method additionally closes listening socket just before passing the control to RequestHandler, since it’s executed in the child process and not in parent.

get_request()
Returns:client socket and client address (IP+port)
Return type:2-tuple (socket, address)

Accept a new connection.

handle_signal(signum, stack_frame)

Signal handler for signal.signal() function. Calls sys.exit(0), in daemon’s main process additionally forwarding signal to all the children.

handle_tick(signum, stack_frame)

Signal handler for SIGALRM signal loop. The handler advances clock in TimeoutQueue, makes it receive any outstanding delayed kill requests, and kills any children that have their timeouts fired (SIGXCPU is the signal used here).

Function sets up another timer to fire in one second.

server_activate()

Prepare server for accepting connections.

server_bind()

Bind server to its socket.

server_close()

Shutdown the server.

set_timeout(timeout)
Parameters:timeout – time after which the process will be killed or None

Request a delayed kill and close the communication channel with parent process.

shutdown_request(client_socket)
Parameters:client_socket – socket where client connection is to be terminated

Properly close the client socket, with notifying the client.

In forking SocketServer, this happens in child (worker) process.

class harpd.daemon.RequestHandler(request, client_address, server)

HarpRPC call request handler.

Attributes defined by parent class:

request

client socket, as returned by SSLServer.get_request() in the first field

client_address

client address (IP, port), as returned by SSLServer.get_request() in the second field

server

SSLServer instance

exception RequestError(type, message, data=None)

Request processing error. Note that this is a different thing than exception raised in called procedure’s code.

struct()

Return the error as a dict suitable for transmitting to client.

exception RequestHandler.Timeout

Signal that execution time expired. This exception is thrown from SIGXCPU handler.

RequestHandler.finish()

Clean up request handler after work.

RequestHandler.handle()

Handle the connection. This means reading client’s request, processing it, and sending results back.

RequestHandler.handle_call(proc_name, arguments)
Parameters:
  • proc_name (string) – name of the procedure to call
  • arguments (list or dict) – arguments (positional or named) to the called procedure

Handle a procedure call request.

RequestHandler.log(message, **context)
Parameters:
  • message – message to log
  • context – additional attributes to attach to the log

Log a message using logging module.

RequestHandler.read_request()
Returns:procedure name, its arguments, and authentication data
Return type:tuple (RequestHandler.Call, RequestHandler.Credentials)
Raise:RequestHandler.RequestError

Read call request from socket.

RequestHandler.send(data, ignore_network_errors=False)
Parameters:data (dict or RequestHandler.RequestError) – response to send

Send response (stream, returned value, exception, error) to client.

RequestHandler.set_timeout(timeout)
Parameters:timeout – timeout or None if no timeout applicable

Setup a timeout for this process.

RequestHandler.setguid(uid, gid)
Parameters:
  • uid (string, integer, or None) – user/UID to change to
  • gid (string, integer, or None) – group/UID to change to

Set UID/GID (and supplementary groups) to whatever was provided. If UID was specified as a name and GID is None, GID defaults to primary group of the user. In any other case None means “don’t change”.

RequestHandler.setup()

Prepare request handler instance for work.

class harpd.daemon.Daemon(pidfile=None, detach=False)

Daemonization helper. If detach was requested, os.fork() is called, the parent process waits for start confirmation (confirm()) and exits with code of 0 (1 if no confirmation was received).

Child process changes its working directory to / and after confirmation, closes its STDIN, STDOUT, and STDERR (this way any uncaught exception is printed to terminal).

Parameters:
  • pidfile – pidfile path
  • detach – whether the daemon should detach from terminal or not
class PidFile(filename)

Handle of a pidfile.

Creating an instance of this class automatically registers close() as atexit handler. See release() if you want to detach your daemon from terminal.

Parameters:filename – pidfile path
close()

Close and delete pidfile, if a pidfile is open.

release()

Close pidfile without deleting it.

To be used in parent process when detaching from terminal.

update()

Update PID stored in pidfile to the one of this process.

Daemon.confirm()

Confirm to parent that initialization was successful and daemon can continue work from here.

Method to be called in child process when detaching from terminal.

class harpd.daemon.TimeoutQueue

Combined queue for delayed kill requests, interprocess channel for such requests, and a system-independent clock.

close()

Close the sockets.

kill_ready()
Returns:list of PIDs

Pop from the queue all the processes that requested being killed.

Note that this queue does not keep track of which children terminated and which are alive. Caller needs to check this manually (a process could have spawned with the same PID as some terminated child).

receive_kill_requests()

Read all the delayed kill requests sent from child processes and add them to internal queue.

Use kill_ready() to check which kill requests are ready to be realized.

static socketpair()
Returns:(read_end, write_end)

Create a pair of AF_UNIX sockets. These sockets are unidirectional.

tick()

Advance internal system-independent clock by one second.

timeout(after)
Parameters:after – number of seconds after which the process should be killed

Request a delayed kill.

Module loader module

Module loader can load Python modules as well as snippets stored in arbitrary files. The latter enables storing configuration as a Python code.

Note that typically it’s a better idea to store configuration in INI, YAML or JSON files than in Python code, because it’s easier to generate and process in other tools and even languages.

class harpd.module.ModuleLoader

Module loader. It can load module from sys.path or a code snippet from outside.

ModuleLoader is a context manager.

close()

Clean up temporary directory. This function is also called on object destruction, so there’s no need (but no harm, either) to call it separately.

load(name, file=None)
Parameters:
  • name – name of the module to load
  • file – module’s file name
Returns:

imported module’s handle

Load specified module and return its handle. Module can be loaded from outside of sys.path (e.g. from /etc) by providing its file name. (In such case, no *.pyc is stored along the original file.)

NOTE: Specifying a name under non-existent hierarchy may cause a warning to be issued. Better stick to a name that exists except for the last component, e.g. harpd.__config__.

Interface for authentication backends

class harpd.auth.AuthDB

Base class for authentication objects. Instance of this class (or rather, a subclass of this class) should be returned by create().

authenticate(user, password)
Returns:True if user + password pair was correct, False otherwise

Check if the password was valid for specified user.

harpd.auth.create(config)
Parameters:config (dict) – parameters read from config
Returns:authentication database object
Return type:AuthDB

Equivalent of this function in an authentication backend module is called to create an auth database for future use.

Communication protocol

Protocol is based on a line-wise JSON. Each message is serialized to a single line. The protocol is synchronous, i.e., client waits for a response after each request.

Since HarpRPC is not planned to be a heavy duty service, requests are intended to be carried in separate connections. Cancelling a request boils down to simply closing the connection before receiving result.

Call request

Request

Call with positional arguments:

{"harp": 1, "procedure": "...", "arguments": [...], "auth": {"user": "...", "password": "..."}}

Call with named arguments:

{"harp": 1, "procedure": "...", "arguments": {...}, "auth": {"user": "...", "password": "..."}}

Call with no arguments:

{"harp": 1, "procedure": "...", "arguments": [], "auth": {"user": "...", "password": "..."}}

Each call request needs to be authenticated. Currently, the only authentication method is username+password pair.

Acknowledgement

Call request is immediately acknowledged with a message indicating whether there will be a streamed result or not. The message looks as follows:

{"harp": 1, "stream_result": true | false}

Transport-level and daemon operational errors

If the request is ill-formatted, contains invalid data, points to a non-existing function, carries incorrect number of arguments, or there are other server-side problems with execution of the procedure, error is returned instead of or after acknowledgement (possibly in the middle of stream result):

{"harp": 1, "error": {"type": "...", "message": "...", "data": ...}}

"type" indicates the kind of the error, in similar fashion as errno in unix system calls (except for being string). "message" is more detailed string that is readable to human. "data" key is optional and may carry any value that could help in troubleshooting. Structured data (i.e. JSON object) is preferred.

Predefined values of "type" are:

  • "parse_error" – request line is not a valid JSON
  • "invalid_protocol""harp" field from the request is missing or carries wrong value
  • "invalid_request" – any other error with request structure
  • "auth_error" – user doesn’t exist or password is invalid
  • "no_such_procedure" – requested procedure does not exist
  • "procedure_loading_error" – requested procedure could not be loaded
  • "invalid_argument_list" – argument list does not match the signature of the requested procedure

Other values of "type" are allowed.

Response

After acknowledgement message, one or more messages with the result is sent.

Single result:

{"result": ...}

Streamed result:

{"stream": ...}
{"stream": ...}
...
{"result": ...}

Exception raised in the procedure is reported by sending following message instead of {"result":...}:

{"exception": {"type": "...", "message": "...", "data": ...}}

Meaning and format of these fields is similar to protocol-level error message. "data" is an optional field here as well.

Streamed result always ends with either {"result":...}, {"exception":...}, or {"error":..., "harp": 1} message.