executorlib 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ from socket import gethostname
2
+ from typing import Optional
3
+
4
+ import cloudpickle
5
+ import zmq
6
+
7
+
8
+ class SocketInterface(object):
9
+ """
10
+ The SocketInterface is an abstraction layer on top of the zero message queue.
11
+
12
+ Args:
13
+ interface (executorlib.shared.interface.BaseInterface): Interface for starting the parallel process
14
+ """
15
+
16
+ def __init__(self, interface=None):
17
+ self._context = zmq.Context()
18
+ self._socket = self._context.socket(zmq.PAIR)
19
+ self._process = None
20
+ self._interface = interface
21
+
22
+ def send_dict(self, input_dict: dict):
23
+ """
24
+ Send a dictionary with instructions to a connected client process.
25
+
26
+ Args:
27
+ input_dict (dict): dictionary of commands to be communicated. The key "shutdown" is reserved to stop the
28
+ connected client from listening.
29
+ """
30
+ self._socket.send(cloudpickle.dumps(input_dict))
31
+
32
+ def receive_dict(self):
33
+ """
34
+ Receive a dictionary from a connected client process.
35
+
36
+ Returns:
37
+ dict: dictionary with response received from the connected client
38
+ """
39
+ output = cloudpickle.loads(self._socket.recv())
40
+ if "result" in output.keys():
41
+ return output["result"]
42
+ else:
43
+ error_type = output["error_type"].split("'")[1]
44
+ raise eval(error_type)(output["error"])
45
+
46
+ def send_and_receive_dict(self, input_dict: dict) -> dict:
47
+ """
48
+ Combine both the send_dict() and receive_dict() function in a single call.
49
+
50
+ Args:
51
+ input_dict (dict): dictionary of commands to be communicated. The key "shutdown" is reserved to stop the
52
+ connected client from listening.
53
+
54
+ Returns:
55
+ dict: dictionary with response received from the connected client
56
+ """
57
+ self.send_dict(input_dict=input_dict)
58
+ return self.receive_dict()
59
+
60
+ def bind_to_random_port(self):
61
+ """
62
+ Identify a random port typically in the range from 49152 to 65536 to bind the SocketInterface instance to. Other
63
+ processes can then connect to this port to receive instructions and send results.
64
+
65
+ Returns:
66
+ int: port the SocketInterface instance is bound to.
67
+ """
68
+ return self._socket.bind_to_random_port("tcp://*")
69
+
70
+ def bootup(
71
+ self,
72
+ command_lst: list[str],
73
+ prefix_name: Optional[str] = None,
74
+ prefix_path: Optional[str] = None,
75
+ ):
76
+ """
77
+ Boot up the client process to connect to the SocketInterface.
78
+
79
+ Args:
80
+ command_lst (list): list of strings to start the client process
81
+ prefix_name (str): name of the conda environment to initialize
82
+ prefix_path (str): path of the conda environment to initialize
83
+ """
84
+ self._interface.bootup(
85
+ command_lst=command_lst, prefix_name=prefix_name, prefix_path=prefix_path
86
+ )
87
+
88
+ def shutdown(self, wait: bool = True):
89
+ result = None
90
+ if self._interface.poll():
91
+ result = self.send_and_receive_dict(
92
+ input_dict={"shutdown": True, "wait": wait}
93
+ )
94
+ self._interface.shutdown(wait=wait)
95
+ if self._socket is not None:
96
+ self._socket.close()
97
+ if self._context is not None:
98
+ self._context.term()
99
+ self._process = None
100
+ self._socket = None
101
+ self._context = None
102
+ return result
103
+
104
+ def __del__(self):
105
+ self.shutdown(wait=True)
106
+
107
+
108
+ def interface_bootup(
109
+ command_lst: list[str],
110
+ connections,
111
+ hostname_localhost: bool = False,
112
+ prefix_name: Optional[str] = None,
113
+ prefix_path: Optional[str] = None,
114
+ ):
115
+ """
116
+ Start interface for ZMQ communication
117
+
118
+ Args:
119
+ command_lst (list): List of commands as strings
120
+ connections (executorlib.shared.interface.BaseInterface): Interface to start parallel process, like MPI, SLURM
121
+ or Flux
122
+ hostname_localhost (boolean): use localhost instead of the hostname to establish the zmq connection. In the
123
+ context of an HPC cluster this essential to be able to communicate to an
124
+ Executor running on a different compute node within the same allocation. And
125
+ in principle any computer should be able to resolve that their own hostname
126
+ points to the same address as localhost. Still MacOS >= 12 seems to disable
127
+ this look up for security reasons. So on MacOS it is required to set this
128
+ option to true
129
+ prefix_name (str): name of the conda environment to initialize
130
+ prefix_path (str): path of the conda environment to initialize
131
+
132
+ Returns:
133
+ executorlib.shared.communication.SocketInterface: socket interface for zmq communication
134
+ """
135
+ if not hostname_localhost:
136
+ command_lst += [
137
+ "--host",
138
+ gethostname(),
139
+ ]
140
+ interface = SocketInterface(interface=connections)
141
+ command_lst += [
142
+ "--zmqport",
143
+ str(interface.bind_to_random_port()),
144
+ ]
145
+ interface.bootup(
146
+ command_lst=command_lst, prefix_name=prefix_name, prefix_path=prefix_path
147
+ )
148
+ return interface
149
+
150
+
151
+ def interface_connect(host: str, port: str):
152
+ """
153
+ Connect to an existing SocketInterface instance by providing the hostname and the port as strings.
154
+
155
+ Args:
156
+ host (str): hostname of the host running the SocketInterface instance to connect to.
157
+ port (str): port on the host the SocketInterface instance is running on.
158
+ """
159
+ context = zmq.Context()
160
+ socket = context.socket(zmq.PAIR)
161
+ socket.connect("tcp://" + host + ":" + port)
162
+ return context, socket
163
+
164
+
165
+ def interface_send(socket: zmq.Socket, result_dict: dict):
166
+ """
167
+ Send results to a SocketInterface instance.
168
+
169
+ Args:
170
+ socket (zmq.Socket): socket for the connection
171
+ result_dict (dict): dictionary to be sent, supported keys are result, error and error_type.
172
+ """
173
+ socket.send(cloudpickle.dumps(result_dict))
174
+
175
+
176
+ def interface_receive(socket: zmq.Socket):
177
+ """
178
+ Receive instructions from a SocketInterface instance.
179
+
180
+ Args:
181
+ socket (zmq.Socket): socket for the connection
182
+ """
183
+ return cloudpickle.loads(socket.recv())
184
+
185
+
186
+ def interface_shutdown(socket: zmq.Socket, context: zmq.Context):
187
+ """
188
+ Close the connection to a SocketInterface instance.
189
+
190
+ Args:
191
+ socket (zmq.Socket): socket for the connection
192
+ context (zmq.sugar.context.Context): context for the connection
193
+ """
194
+ socket.close()
195
+ context.term()