-
Notifications
You must be signed in to change notification settings - Fork 0
stdrpc
Provides support for Remote Procedure Calls (RPC) in FunL. This provides way for FunL inter-process communication. RPC works in client-server model. Client calls remotely procedure execution in another process by name (string) and providing arguments for call. Server executes procedure call and replies return value to client. Client and server are defined by using opaque values (opaque:rserver and opaque:rproxy) so process can act as client and server simultaneously by using those values.
All values (arguments and return value) are encoded and decoded before/after going "to the wire". Values can be any serializable FunL values.
RPC mechanism in general has been (rightfully so) critized for trying to hide differences between local and external/distributed communication that really cannot be hidden (failure mode, delays). Because of this stdrpc is very explicit about using remote (inter-process) communication so that it's clearly different from calling functions/procedures locally.
Implementation of stdrpc is based on using HTTP as transport mechanism and JSON encoding/decoding. HTTP communication is RPC kind of already but stdrpc makes usage easier as HTTP details are hidden and encoding/decoding values is automatically done. Couple of basic assumptions in stdrpc design were:
- there is no separate server process(es) needed (design target)
- processes using stdrpc are implemented in FunL language or in Go
If runtime error happens during remote procedure execution then error is returned to client.
RPC servers can be implemented also with Go language, see Example of RPC server in Go
Client process uses rproxy for making RPC calls. Client creates rproxy value by giving address (string) of server to it.
Server process creates rserver by giving address in which server Goroutine listens incoming RPC requests (Goroutine is created to background for receiving RPC).
Creates server value. Address to listen is given as argument (string).
type: procedure
Format:
call(stdrpc.new-server <address:string>) -> <list: is-ok error-description opaque:rserver>
Return value: list
Registers procedure/function (3rd argument) with name (2nd argument) to rserver (1st argument).
type: procedure
Format:
call(stdrpc.register <opaque:rserver> <name:string> <proc/func>) -> <list: is-ok error-description>
Return value: list
Unregisters procedure/function from rserver (1st argument). If no 2nd argument is given then all procedures/functions are unregistered from rserver. If 2nd argument is given (name, string) then corresponding procedure/function is unregistered.
type: procedure
Format:
call(stdrpc.unregister <opaque:rserver>) -> <list: is-ok error-description>
call(stdrpc.unregister <opaque:rserver> <name:string>) -> <list: is-ok error-description>
Return value: list
Creates rproxy for client. Address of corresponding server is given as argument (string).
type: procedure
Format:
call(stdrpc.new-proxy <address:string>) -> <opaque:rproxy>
Return value: opaque:rproxy
Client calls this to make remote procedure call. Related rproxy is given as 1st argument. Name of procedure (registered name) is given as 2nd argument. After that arguements for RPC are given as separate arguments.
type: procedure
Format:
call(stdrpc.rcall <opaque:rproxy> <name:string> <arg-1:value> <arg-2:value> ...) -> <list: is-ok error-description <return-value>>
Return value: list
Shuts down rserver. Ongoing remote procedure calls are executed to completion and after that rserver closes.
type: procedure
Format:
call(stdrpc.close <opaque:rserver>) -> <list: is-ok error-description>
Return value: list
Example of using RPC:
Client implementation (rclient.fnl):
ns main
main = proc()
import stdrpc
px = call(stdrpc.new-proxy 'localhost:9901')
list(
call(stdrpc.rcall px 'remote-proc-1' 10 20)
call(stdrpc.rcall px 'remote-proc-2' 10 20)
)
end
endns
Server implementation (rserver.fnl):
ns main
rproc-1 = proc(p1 p2)
_ = print(sprintf('1: received: %v, %v' p1 p2))
plus(p1 p2)
end
rproc-2 = proc(p1 p2)
_ = print(sprintf('2: received: %v, %v' p1 p2))
mul(p1 p2)
end
just-waiting = proc()
import stdtime
call(proc()
while( call(proc() _ = print('...waiting...') _ = call(stdtime.sleep 10) true end) 'none')
end)
end
main = proc()
import stdrpc
import stddbc
ok err server = call(stdrpc.new-server 'localhost:9901'):
_ = call(stddbc.assert ok err)
_ = call(stdrpc.register server 'remote-proc-1' rproc-1)
_ = call(stdrpc.register server 'remote-proc-2' rproc-2)
call(just-waiting)
end
endns
Running server:
./funla rserver.fnl
...waiting...
1: received: 10, 20
2: received: 10, 20
...waiting...
Running client:
./funla rclient.fnl
list(list(true, '', 30), list(true, '', 200))
Following things could be implemented in future:
- support for secure communication (TLS)
- options for proxy (map) -> time limit for RPC completion etc.