Skip to content

Commit a1398da

Browse files
authored
Merge pull request #200 from alphaville/feature/198
TCP server with dynamically configurable ip and port
2 parents 2c9e233 + ffa6d00 commit a1398da

13 files changed

Lines changed: 265 additions & 96 deletions

File tree

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ before_install:
7575
install:
7676
- bash ci/install.sh
7777

78+
before_script:
79+
- rustup component add clippy
80+
7881
script:
7982
- bash ci/script.sh
8083

ci/script.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ regular_test() {
66
# ------------------------------------
77
cargo test
88

9+
# Run Clippy
10+
# ------------------------------------
11+
cargo clippy --all-targets --all-features
912

1013
# Run Python tests
1114
# ------------------------------------
@@ -29,6 +32,16 @@ regular_test() {
2932
export PYTHONPATH=.
3033
python -W ignore test/test_constraints.py -v
3134
python -W ignore test/test.py -v
35+
36+
37+
# Run Clippy for generated optimizers
38+
# ------------------------------------
39+
cd .python_test_build/only_f1/tcp_iface_only_f1
40+
cargo clippy --all-targets --all-features
41+
cd ../../only_f2/tcp_iface_only_f2
42+
cargo clippy --all-targets --all-features
43+
cd ../../rosenbrock_ros
44+
cargo clippy --all-targets --all-features
3245
}
3346

3447
test_docker() {

docs/python-advanced.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ for requests (e.g., for remote connections), you may crate an
126126
```python
127127
tcp_config = og.config.TcpServerConfiguration('10.8.0.12', 9555)
128128
```
129-
129+
130130
and then provide it to the builder configuration using
131131

132132
```python
@@ -138,10 +138,13 @@ builder_config.with_tcp_interface_config(tcp_config)
138138
There are two ways to connect to a generated TCP server and call the
139139
auto-generated optimizer:
140140

141-
1. *Connect to a local optimizer* by providing the path of the optimizer
141+
<div class="alert alert-info">
142+
<b>Connect to a local optimizer</b> by providing the path of the optimizer
142143
directory. For that purpose, we need to create an instance of
143-
`OptimizerTcpManager` and specify the path to the auto-generated optimizer;
144-
for example,
144+
<code>OptimizerTcpManager</code> and specify the path to the auto-generated optimizer.
145+
</div>
146+
147+
For example,
145148

146149
```python
147150
mng = og.tcp.OptimizerTcpManager('python_build/the_optimizer')
@@ -150,10 +153,28 @@ mng = og.tcp.OptimizerTcpManager('python_build/the_optimizer')
150153
we can then `start` the optimizer. The TCP manager known what IP and port
151154
to link to, so we can `call` it directly.
152155

153-
2. *Connect to a remote optimizer* by providing its IP and port. In that
156+
<div class="alert alert-info">
157+
<b>Connect to a local optimizer</b> and <b>customize</b> its IP and port.
158+
This is particularly useful if you need to start multiple instances of a TCP
159+
server (with different ports).
160+
</div>
161+
162+
For example,
163+
164+
```python
165+
ip = '0.0.0.0'
166+
port = 5678
167+
mng = og.tcp.OptimizerTcpManager('python_build/the_optimizer', ip, port)
168+
```
169+
170+
<div class="alert alert-info">
171+
<b>Connect to a remote optimizer</b> by providing its IP and port. In that
154172
case we assume that an optimizer is up an running at some remote address
155173
and listens for connections at a certain port. In that case, we cannot
156-
`start` the optimizer remotely using the TCP manager.
174+
<code>start</code> the optimizer remotely using the TCP manager.
175+
</div>
176+
177+
157178

158179
For example to connect to a *remote* TCP server at `10.8.0.7:5678`, we can
159180
create a TCP manager as follows:

open-codegen/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
Note: This is the Changelog file of `opengen` - the Python interface of OpEn
99

10+
## [0.6.1] - 2020-09-10
11+
12+
### Changed
13+
14+
* `OptimizerTcpManager`: ip and port can be set dynamically
15+
16+
1017
## [0.6.0] - 2020-09-03
1118

1219
### Added
@@ -55,6 +62,7 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn
5562
* Fixed `lbfgs` typo
5663

5764

65+
[0.6.1]: https://github.com/alphaville/optimization-engine/compare/opengen-v0.6.0...opengen-0.6.1
5866
[0.6.0]: https://github.com/alphaville/optimization-engine/compare/opengen-v0.5.0...opengen-0.6.0
5967
[0.5.0]: https://github.com/alphaville/optimization-engine/compare/opengen-0.4.1...opengen-v0.5.0
6068
[0.4.1]: https://github.com/alphaville/optimization-engine/compare/opengen-0.4.1...master

open-codegen/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.6.0
1+
0.6.1

open-codegen/opengen/main.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import time
2+
import logging
13
import casadi.casadi as cs
24
import opengen as og
35

6+
logging.getLogger().setLevel(5)
7+
48
u = cs.SX.sym("u", 5) # decision variable (nu = 5)
59
p = cs.SX.sym("p", 2) # parameter (np = 2)
610
phi = cs.dot(u, u) # cost function
@@ -11,18 +15,39 @@
1115
.with_constraints(bounds)
1216

1317
meta = og.config.OptimizerMeta() \
14-
.with_optimizer_name("halfspace_optimizer")
18+
.with_optimizer_name("halfspace_optimizer") \
19+
.with_authors(["P. Sopasakis", "S. Author"]).with_version("0.1.56")
1520

1621
tcp_config = og.config.TcpServerConfiguration(bind_port=3305)
1722
build_config = og.config.BuildConfiguration() \
1823
.with_build_directory('my_optimizers') \
1924
.with_build_mode(og.config.BuildConfiguration.DEBUG_MODE) \
20-
.with_open_version('0.7.1-alpha') \
21-
.with_tcp_interface_config(tcp_interface_config=tcp_config)
25+
.with_tcp_interface_config()
2226

2327
builder = og.builder.OpEnOptimizerBuilder(problem,
2428
meta,
2529
build_config,
2630
og.config.SolverConfiguration())
2731
builder.build()
2832

33+
all_managers = []
34+
for i in range(10):
35+
all_managers += [og.tcp.OptimizerTcpManager(
36+
optimizer_path='my_optimizers/halfspace_optimizer',
37+
ip='0.0.0.0',
38+
port=3311+i)]
39+
40+
for m in all_managers:
41+
m.start()
42+
43+
time.sleep(4)
44+
45+
for m in all_managers:
46+
print(m.details)
47+
resp = m.call(p=[1., 2.])
48+
print(resp.get().solution)
49+
50+
# mng.kill()
51+
time.sleep(6)
52+
for m in all_managers:
53+
m.kill()

open-codegen/opengen/tcp/optimizer_tcp_manager.py

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,54 +20,70 @@ class OptimizerTcpManager:
2020
"""
2121

2222
def __init__(self, optimizer_path=None, ip=None, port=None):
23-
"""Constructs instance of <code>OptimizerTcpManager</code>
23+
"""
24+
Constructs instance of <code>OptimizerTcpManager</code>
25+
26+
There are three ways to use this constructor:
27+
28+
- OptimizerTcpManager(optimizer_path): creates a TCP manager for a local
29+
TCP server using the default IP and port of that TCP server (specified
30+
upon code generation)
31+
- OptimizerTcpManager(optimizer_path, ip, port): creates a TCP manager
32+
for a local TCP server, but overrides the default IP and port. This way
33+
the user can set the address '0.0.0.0', so that the TCP server binds on
34+
all IPs, or '127.0.0.1' so that it is accessible only locally, or a VPN
35+
IP address, so that the optimizer is accessible only over a private
36+
network.
37+
- OptimizerTcpManager(ip, port): If a path is not provided, then the
38+
TCP manager can be used to connect to a remote TCP server, as a client,
39+
but cannot be used to start the server.
2440
2541
Args:
26-
optimizer_path: path to auto-generated optimizer (just to
27-
be clear: this is the folder that contains <code>optimizer.yml</code>)
42+
:param optimizer_path:
43+
path to auto-generated optimizer (just to be clear: this is
44+
the folder that contains <code>optimizer.yml</code>)
45+
46+
:param ip:
47+
the user can provide the IP of a remote TCP server (must be up and
48+
running) so as to establish a remote connection. In that case `path`
49+
must be equal to `None` (see examples above)
50+
51+
:param port: see ip
2852
2953
Returns:
3054
New instance of <code>OptimizerTcpManager</code>
3155
"""
3256
self.__optimizer_path = optimizer_path
3357
if optimizer_path is not None:
34-
self.__optimizer_details_from_yml = None
58+
self.__optimizer_details = None # create attribute (including IP and port)
3559
self.__load_tcp_details()
60+
if ip is not None:
61+
self.__optimizer_details['tcp']['ip'] = ip
62+
if port is not None:
63+
self.__optimizer_details['tcp']['port'] = port
3664
elif ip is not None and port is not None:
37-
self.__optimizer_details_from_yml = {"tcp": {"ip": ip, "port": port}}
65+
self.__optimizer_details = {"tcp": {"ip": ip, "port": port}}
3866
else:
3967
raise Exception("Illegal arguments")
4068
# Check whether the optimizer was built with the current version of opengen
41-
opengen_version = self.__optimizer_details_from_yml['build']['opengen_version']
69+
opengen_version = self.__optimizer_details['build']['opengen_version']
4270
current_opengen_version = pkg_resources.require("opengen")[0].version
4371
if current_opengen_version != opengen_version:
4472
logging.warn('the target optimizer was build with a different version of opengen (%s)' % opengen_version)
4573
logging.warn('you are running opengen version %s' % current_opengen_version)
4674

75+
logging.info("TCP/IP details: %s:%d",
76+
self.__optimizer_details['tcp']['ip'],
77+
self.__optimizer_details['tcp']['port'])
78+
4779
def __load_tcp_details(self):
48-
logging.info("loading TCP/IP details")
4980
yaml_file = os.path.join(self.__optimizer_path, "optimizer.yml")
5081
with open(yaml_file, 'r') as stream:
51-
self.__optimizer_details_from_yml = yaml.safe_load(stream)
52-
details = self.__optimizer_details_from_yml
53-
logging.info("TCP/IP details: %s:%d", details['tcp']['ip'], details['tcp']['port'])
54-
55-
def __threaded_start(self):
56-
optimizer_details = self.__optimizer_details_from_yml
57-
logging.info("Starting TCP/IP server at %s:%d (in a detached thread)",
58-
optimizer_details['tcp']['ip'],
59-
optimizer_details['tcp']['port'])
60-
command = ['cargo', 'run', '-q']
61-
if optimizer_details['build']['build_mode'] == 'release':
62-
command.append('--release')
63-
tcp_dir_name = "tcp_iface_" + optimizer_details['meta']['optimizer_name']
64-
tcp_iface_directory = os.path.join(self.__optimizer_path, tcp_dir_name)
65-
p = subprocess.Popen(command, cwd=tcp_iface_directory)
66-
p.wait()
82+
self.__optimizer_details = yaml.safe_load(stream)
6783

6884
@retry(tries=10, delay=1)
6985
def __obtain_socket_connection(self):
70-
tcp_data = self.__optimizer_details_from_yml
86+
tcp_data = self.__optimizer_details
7187
ip = tcp_data['tcp']['ip']
7288
port = tcp_data['tcp']['port']
7389
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
@@ -101,31 +117,62 @@ def ping(self):
101117
return json.loads(data)
102118

103119
def __check_if_server_is_running(self):
104-
tcp_data = self.__optimizer_details_from_yml
120+
tcp_data = self.__optimizer_details
105121
ip = tcp_data['tcp']['ip']
106122
port = tcp_data['tcp']['port']
107123
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
108124
return 0 == s.connect_ex((ip, port))
109125

126+
@property
127+
def details(self):
128+
return self.__optimizer_details
129+
110130
def start(self):
111-
"""Starts the TCP server"""
112-
# start the server in a separate thread
131+
"""Starts the TCP server
132+
133+
Note: this method starts a *local* server whose path must have been
134+
provided - we cannot start a remote server.
135+
136+
The server starts on a separate thread, so this method does not block
137+
the execution of the caller's programme.
113138
139+
"""
140+
141+
# Check if a path has been provided; if not,
114142
if self.__optimizer_path is None:
115143
raise Exception("No optimizer path provided - cannot start a remote server")
116144

145+
# Server start data
146+
tcp_data = self.__optimizer_details
147+
ip = tcp_data['tcp']['ip']
148+
port = tcp_data['tcp']['port']
149+
150+
# Check if any of the ip/port pairs is occupied
117151
if self.__check_if_server_is_running():
118-
msg = "Port %d not available" % self.__optimizer_details_from_yml['tcp']['port']
152+
msg = "Port %d not available" % port
119153
raise Exception(msg)
120154

155+
def threaded_start():
156+
optimizer_details = self.__optimizer_details
157+
logging.info("Starting TCP/IP server at %s:%d (in a detached thread)",
158+
ip, port)
159+
command = ['cargo', 'run', '-q', '--', '--port=%d' % port, '--ip=%s' % ip]
160+
if optimizer_details['build']['build_mode'] == 'release':
161+
command.append('--release')
162+
tcp_dir_name = "tcp_iface_" + optimizer_details['meta']['optimizer_name']
163+
tcp_iface_directory = os.path.join(self.__optimizer_path, tcp_dir_name)
164+
p = subprocess.Popen(command, cwd=tcp_iface_directory)
165+
p.wait()
166+
167+
# start the server in a separate thread
121168
logging.info("Starting TCP/IP server thread")
122-
thread = Thread(target=self.__threaded_start)
169+
thread = Thread(target=threaded_start)
123170
thread.start()
124171

125172
# ping the server until it responds so that we know it's
126173
# up and running
127174
logging.info("Waiting for server to start")
128-
time.sleep(2)
175+
time.sleep(0.1)
129176
self.ping()
130177

131178
def kill(self):

0 commit comments

Comments
 (0)