Server/Client I/O

Often you will want to call one model from another like a functions. This would required sending the input variable(s) to the model being called and then sending the output variables(s) back to the calling model. We refer to this as a Remote Procedure Call (RPC). The model being called can be considered a server providing its calculations as a service to the client (the calling model). The yggdrasil provides options for treating models as server and clients.

One Server, One Client

In the example below, the “server” model computes the nth number in the Fibonacci sequence and the “client” model calls the server to get a certain portion of the Fibonacci sequences and then writes it to a log. The server will continue processing requests until all clients connected to it have disconnected.

Model Code:

 1import numpy as np
 2from yggdrasil.interface.YggInterface import YggInput, YggRpcServer
 3
 4
 5def get_fibonacci(n):
 6    r"""Compute the nth number of the Fibonacci sequence.
 7
 8    Args:
 9        n (int): Index of the Fibonacci number in the Fibonacci sequence that
10            should be returned.
11
12    Returns:
13        int: The nth Fibonacci number.
14
15    """
16    pprev = 0
17    prev = 1
18    result = 1
19    fib_no = 1
20    while fib_no < n:
21        result = prev + pprev
22        pprev = prev
23        prev = result
24        fib_no = fib_no + 1
25    return result
26
27
28def main():
29    r"""Function to execute server communication and computation in a loop."""
30
31    print('Hello from Python server!')
32
33    # Get parameters
34    inp = YggInput("params")
35    retval, params = inp.recv()
36    if not retval:
37        raise RuntimeError('server(P): ERROR receiving parameters')
38    print('server(P): Parameters = %s' % params)
39
40    # Create server-side rpc conneciton using model name
41    rpc = YggRpcServer("server", "%d", "%d")
42
43    # Continue receiving requests until the connection is closed when all
44    # clients have disconnected.
45    while True:
46        print('server(P): receiving...')
47        retval, rpc_in = rpc.recv()
48        if not retval:
49            print('server(P): end of input')
50            break
51
52        # Compute fibonacci number
53        n = rpc_in[0]
54        print('server(P): Received request for Fibonacci number %d' % n)
55        result = get_fibonacci(n)
56        print('server(P): Sending response for Fibonacci number %d: %d' % (n, result))
57
58        # Send response back
59        flag = rpc.send(np.int32(result))
60        if not flag:
61            raise RuntimeError('server(P): ERROR sending')
62
63    print('Goodbye from Python server')
64
65    
66if __name__ == '__main__':
67    main()
 1import sys
 2import numpy as np
 3from yggdrasil.interface.YggInterface import (
 4    YggRpcClient, YggOutput)
 5
 6
 7def main(iterations):
 8    r"""Function to execute client communication with server that computes
 9    numbers in the Fibonacci sequence.
10
11    Args:
12        iterations (int): The number of Fibonacci numbers to log.
13
14    """
15
16    print('Hello from Python client: iterations = %d ' % iterations)
17
18    # Set up connections matching yaml
19    # RPC client-side connection will be $(server_name)_$(client_name)
20    rpc = YggRpcClient("server_client", "%d", "%d")
21    log = YggOutput("output_log", 'fib(%-2d) = %-2d\n')
22
23    # Iterate over Fibonacci sequence
24    for i in range(1, iterations + 1):
25        
26        # Call the server and receive response
27        print('client(Python): Calling fib(%d)' % i)
28        ret, result = rpc.call(np.int32(i))
29        if not ret:
30            raise RuntimeError('client(Python): RPC CALL ERROR')
31        fib = result[0]
32        print('client(Python): Response fib(%d) = %d' % (i, fib))
33
34        # Log result by sending it to the log connection
35        ret = log.send(np.int32(i), fib)
36        if not ret:
37            raise RuntimeError('client(Python): SEND ERROR')
38
39    print('Goodbye from Python client')
40
41    
42if __name__ == '__main__':
43    # Take number of iterations from the first argument
44    main(int(sys.argv[1]))

(Example in other languages)

The interface server-side API call (YggRpcServer for Python), requires 3 input variables: the name of the server channel (this will be the name of the server model), a format string for input to the server model, and a format string for output from the server model. The client-side API call (YggRpcClient for Python), also requires 3 input variables: the name of the client channel (this is the name of the server model joined with the name of the client model by an underscore, <server>_<client>, a format string for input to the server model, and a format string for output form the server model. The last two arguments (the format strings) to both the server and client API calls should be the same.

In the server model YAML, the key/value pair is_server: True needs to be added to the model entry to indicate that the model will be called as a server and requires a set of RPC channels. In the client model YAML, the key/value pair client_of: <server_model_name> is required to indicate that the model will act as a client of the <server_model_name> model.

Model YAML:

 1---
 2
 3model:
 4  name: server
 5  language: python
 6  args: ./src/server.py
 7  is_server: True  # Creates a RPC server queue called "server"
 8  inputs:
 9    name: params
10    default_file:
11      name: ./Input/server_params.txt
12      filetype: ascii
 1---
 2
 3model:
 4  name: client
 5  language: python
 6  args:
 7    - ./src/client.py
 8    - 3  # Pass the number of iterations that should be performed
 9  client_of: server  # Creates an RPC client queue "server_client"
10  outputs: output_log
11
12connections:
13  input: output_log
14  output: client_output.txt
15  in_temp: True

(Example in other languages)

In addition to the RPC API call, the example server also has an input params. Models acting as servers can have as many inputs/outputs as desired in addition to the RPC connections. While the example input is not used to modify the output in this example, such a comm could be used to initialize a model with parameters and/or initial conditions.

Using Existing Inputs/Outputs

Models that have already been integrated via yggdrasil can also be turned into servers without modifying the code. Instead of passing a boolean to the is_server parameter, such models can provide a mapping with input and output parameters that explicitly outline which of a existing model’s inputs/outputs should be used for the RPC call. Receive/send calls to named input/output channels will then behave as receive/send calls on a server interface comm.

Todo

Example source code and YAML of server replacing an existing input/output

One Server, Two Clients

There is no limit on the number of clients that can connect to a single server. In the example below, the server is the same as above. The client code is also essentially the same except that it has been modified to take a client_index variable that provides information to differentiates between two clients using the same source code.

Model Code:

 1import numpy as np
 2from yggdrasil.interface.YggInterface import YggInput, YggRpcServer
 3
 4
 5def get_fibonacci(n):
 6    r"""Compute the nth number of the Fibonacci sequence.
 7
 8    Args:
 9        n (int): Index of the Fibonacci number in the Fibonacci sequence that
10            should be returned.
11
12    Returns:
13        int: The nth Fibonacci number.
14
15    """
16    pprev = 0
17    prev = 1
18    result = 1
19    fib_no = 1
20    while fib_no < n:
21        result = prev + pprev
22        pprev = prev
23        prev = result
24        fib_no = fib_no + 1
25    return result
26
27
28def main():
29    r"""Function to execute server communication and computation in a loop."""
30
31    print('Hello from Python server!')
32
33    # Get parameters
34    inp = YggInput("params")
35    retval, params = inp.recv()
36    if not retval:
37        raise RuntimeError('server: ERROR receiving parameters')
38    print('server: Parameters = %s' % params)
39
40    # Create server-side rpc conneciton using model name
41    rpc = YggRpcServer("server", "%d", "%d")
42
43    # Continue receiving requests until the connection is closed when all
44    # clients have disconnected.
45    while True:
46        print('server: receiving...')
47        retval, rpc_in = rpc.recv()
48        if not retval:
49            print('server: end of input')
50            break
51
52        # Compute fibonacci number
53        n = rpc_in[0]
54        print('server: Received request for Fibonacci number %d' % n)
55        result = get_fibonacci(n)
56        print('server: Sending response for Fibonacci number %d: %d' % (n, result))
57
58        # Send response back
59        flag = rpc.send(np.int32(result))
60        if not flag:
61            raise RuntimeError('server: ERROR sending')
62
63    print('Goodbye from Python server')
64
65    
66if __name__ == '__main__':
67    main()
 1import sys
 2import numpy as np
 3from yggdrasil.interface.YggInterface import (
 4    YggRpcClient, YggOutput)
 5
 6
 7def main(iterations, client_index):
 8    r"""Function to execute client communication with server that computes
 9    numbers in the Fibonacci sequence.
10
11    Args:
12        iterations (int): The number of Fibonacci numbers to log.
13        client_index (int): Index of the client in total list of clients.
14
15    """
16
17    print('Hello from Python client%d: iterations = %d ' % (client_index,
18                                                            iterations))
19
20    # Set up connections matching yaml
21    # RPC client-side connection will be $(server_name)_$(client_name)
22    rpc = YggRpcClient("server_client%d" % client_index, "%d", "%d")
23    log = YggOutput("output_log%d" % client_index, 'fib(%-2d) = %-2d\n')
24
25    # Iterate over Fibonacci sequence
26    for i in range(1, iterations + 1):
27        
28        # Call the server and receive response
29        print('client%d(Python): Calling fib(%d)' % (client_index, i))
30        ret, result = rpc.call(np.int32(i))
31        if not ret:
32            raise RuntimeError('client%d(Python): RPC CALL ERROR' % client_index)
33        fib = result[0]
34        print('client%d(Python): Response fib(%d) = %d' % (client_index, i, fib))
35
36        # Log result by sending it to the log connection
37        ret = log.send(np.int32(i), fib)
38        if not ret:
39            raise RuntimeError('client%d(Python): SEND ERROR' % client_index)
40
41    print('Goodbye from Python client%d' % client_index)
42
43    
44if __name__ == '__main__':
45    # Take number of iterations from the first argument and the
46    # client index from the second
47    main(int(sys.argv[1]), int(sys.argv[2]))

(Example in other languages)

The server YAML is the same as above. The client YAML now has entries for two models which are both clients of the server model and call the same source code.

Model YAML:

 1---
 2
 3model:
 4  name: server
 5  language: python
 6  args: ./src/server.py
 7  is_server: True  # Creates a RPC server queue called "server"
 8  inputs:
 9    name: params
10    default_file:
11      name: ./Input/server_params.txt
12      filetype: ascii
 1---
 2
 3models:
 4  - name: client1
 5    language: python
 6    args:
 7      - ./src/client.py
 8      - 3  # Pass the number of iterations that should be performed
 9      - 1  # Pass index of the client
10    client_of: server  # Creates an RPC client queue "server_client"
11    outputs: output_log1
12  - name: client2
13    language: python
14    args:
15      - ./src/client.py
16      - 5  # Pass the number of iterations that should be performed
17      - 2  # Pass index of the client
18    client_of: server  # Creates an RPC client queue "server_client"
19    outputs: output_log2
20
21connections:
22  - input: output_log1
23    output: client_output1.txt
24    in_temp: true
25  - input: output_log2
26    output: client_output2.txt
27    in_temp: true

(Example in other languages)

During runtime, request messages from both clients will be routed to the server model which will process the requests in the order they are received.

Two Servers, Two Clients

There is also no limit on the number of copies of a server model that can be used to responsd to RPC requests from the clients. In the example below, the server and clients are the same as above, but 2 copies of the server model are run as specified by the model copies parameter in the server YAML.

Model YAML:

 1---
 2
 3model:
 4  name: server
 5  language: python
 6  args: ./src/server.py
 7  is_server: True  # Creates a RPC server queue called "server"
 8  copies: 2
 9  inputs:
10    name: params
11    default_file:
12      name: ./Input/server_params.txt
13      filetype: ascii
 1---
 2
 3models:
 4  - name: client1
 5    language: python
 6    args:
 7      - ./src/client.py
 8      - 3  # Pass the number of iterations that should be performed
 9      - 1  # Pass index of the client
10    client_of: server  # Creates an RPC client queue "server_client"
11    outputs: output_log1
12  - name: client2
13    language: python
14    args:
15      - ./src/client.py
16      - 5  # Pass the number of iterations that should be performed
17      - 2  # Pass index of the client
18    client_of: server  # Creates an RPC client queue "server_client"
19    outputs: output_log2
20
21connections:
22  - input: output_log1
23    output: client_output1.txt
24    in_temp: true
25  - input: output_log2
26    output: client_output2.txt
27    in_temp: true

(Example in other languages)

This allow client requests to be returned twice as fast, but precludes any use of an internal state by the server model as there is no way for a client to be sure that the same server model is responding to its requests and only its requests.

Wrapped Function Server

Models that are created by letting yggdrasil automatically wrap a function can also act as servers and/or clients. In the example below, the model acting as a server is a very simple function that takes a string as an input and returns the same string and the client is a function that takes a string as an input, calls the server models with the input string and returns the response.

When a client model is autowrapped from a function, additional care must be taken so that the client RPC comm can be reused during each call to the model. In interpreted models (Python, R, MATLAB), this is done by passing the keyword global_scope to the RPC client interface initialization function (YggRpcClient in Python). In compiled models (C, C++, Fortran), this is done by framing RPC client interface initialization calls with the WITH_GLOBAL_SCOPE macro (see the language specific versions of this example for specifics).

Model Code:

1def model_function(in_buf):
2    print("server(Python): %s" % in_buf)
3    out_buf = in_buf
4    return out_buf
 1from yggdrasil.languages.Python.YggInterface import YggRpcClient
 2
 3
 4def model_function(in_buf):
 5    # The global_scope keyword is required to ensure that the comm persists
 6    # between function calls
 7    rpc = YggRpcClient('server_client', global_scope=True)
 8    print("client(Python): %s" % in_buf)
 9    ret, result = rpc.call(in_buf)
10    if not ret:
11        raise RuntimeError('client(Python): RPC CALL ERROR')
12    out_buf = result
13    return out_buf

(Example in other languages)

The RPC connection between the server and the client is controlled by the same is_server and client_of YAML parameters as before.

Model YAML:

1model:
2  name: server
3  language: python
4  args: ./src/server.py
5  function: model_function
6  is_server: True
 1model:
 2  name: client
 3  language: python
 4  args: ./src/client.py
 5  function: model_function
 6  client_of: server
 7  inputs:
 8    name: in_buf
 9    default_file:
10      name: ./Input/input.txt
11      filetype: ascii
12  outputs:
13    name: out_buf
14    default_file:
15      name: ./client_output.txt
16      in_temp: true

(Example in other languages)

By default, all inputs to a wrapped server function will be used in the RPC call. However if only some of the inputs should be passed in by the RPC calls, they can be specified explicitly by providing the is_server parameter with a map that contains input and output parameters that map to the names of function input/output variables (as in the case of using existing input/output channels above).