LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ag_worker_client.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_AG_AG_WORKER_CLIENT_H
6#define LINE_SOLVERS_AG_AG_WORKER_CLIENT_H
7
8/**
9 * @file ag_worker_client.h
10 * @ingroup line_solvers
11 * @brief Coordinator-side connections to the ag-worker processes.
12 *
13 * Speaks the protocol of `jline.solvers.ag.AgWire`: newline-delimited JSON over
14 * a plain TCP socket, one object per line. `assign` ships the STATIC half of
15 * each owned agent -- its local rate matrix and the passive/active matrices of
16 * the actions it takes part in -- once per solve, because none of it changes
17 * across the fixed point; `sweep` then carries only the reversed rates, one
18 * double per action, which is the entire coupling between agents.
19 *
20 * A worker that cannot be reached, or that fails mid-run, is marked dead and its
21 * agents fall back to the coordinator. That is not a defensive workaround but a
22 * property of the decomposition: an agent depends on the rest of the model only
23 * through the reversed rates, so anyone holding them can solve it.
24 */
25
26#include <array>
27#include <cerrno>
28#include <cstddef>
29#include <cstring>
30#include <iostream>
31#include <stdexcept>
32#include <string>
33#include <vector>
34
35#include <arpa/inet.h>
36#include <netdb.h>
37#include <netinet/in.h>
38#include <sys/socket.h>
39#include <sys/time.h>
40#include <unistd.h>
41
42#include "json.hpp"
43
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace ag {
49
50/** One agent's static description, in the wire form the worker expects. */
52 int k = 0;
53 int n = 0;
54 int mph = 1;
55 int nlev = 1;
56 std::vector<int> level;
57 /** [row, col, value] triplets, 0-based. */
58 std::vector<std::array<double, 3>> L;
59 std::vector<int> passive_c;
60 std::vector<std::vector<std::array<double, 3>>> passive_m;
61 std::vector<int> active_c;
62 std::vector<std::vector<std::array<double, 3>>> active_m;
63};
64
65/** Non-zero entries of M as [row, col, value] triplets, 0-based. */
66inline std::vector<std::array<double, 3>> ag_triplets(const Matrix<double>& m) {
67 std::vector<std::array<double, 3>> out;
68 for (std::size_t i = 0; i < m.rows(); ++i) {
69 for (std::size_t j = 0; j < m.cols(); ++j) {
70 const double v = m(i, j);
71 if (v != 0.0) {
72 out.push_back({static_cast<double>(i), static_cast<double>(j), v});
73 }
74 }
75 }
76 return out;
77}
78
79/** The connections to every configured worker, plus the agent partition. */
81 public:
82 AgWorkerPool(const std::vector<std::string>& endpoints, double timeout_seconds)
83 : endpoints_(endpoints), timeout_(timeout_seconds) {
84 if (endpoints_.empty()) {
85 throw InputError(
86 "ag: the 'cluster' execution backend needs worker endpoints: set "
87 "AgOptions::endpoints to \"host:port\" strings, each one an ag-worker "
88 "started with 'java -cp jline.jar jline.cli.AgWorker -p <port>'");
89 }
90 fds_.assign(endpoints_.size(), -1);
91 live_.assign(endpoints_.size(), true);
92 }
93
94 ~AgWorkerPool() { close_all(); }
95
96 AgWorkerPool(const AgWorkerPool&) = delete;
98
99 /**
100 * Connect and ship the static half of each owned agent, once. @p payload maps
101 * an agent index to its wire description.
102 */
103 template <class Payload>
104 void ensure_assigned(std::size_t num_agents, Payload payload) {
105 if (assigned_) return;
106 partition(num_agents);
107 for (std::size_t w = 0; w < endpoints_.size(); ++w) {
108 if (owns_[w].empty()) continue;
109 try {
110 connect(w);
111 nlohmann::json agents = nlohmann::json::array();
112 for (std::size_t t = 0; t < owns_[w].size(); ++t) {
113 agents.push_back(encode(payload(owns_[w][t])));
114 }
115 nlohmann::json msg;
116 msg["op"] = "assign";
117 msg["agents"] = agents;
118 const nlohmann::json reply = call(w, msg);
119 if (reply.value("op", std::string()) != "assigned") {
120 throw std::runtime_error("worker did not acknowledge the assignment");
121 }
122 } catch (const std::exception& e) {
123 std::cerr << "[LINE] Warning: AG worker " << endpoints_[w]
124 << " is unreachable (" << e.what()
125 << "); its agents run on the coordinator instead" << std::endl;
126 live_[w] = false;
127 close_one(w);
128 }
129 }
130 assigned_ = true;
131 }
132
133 /**
134 * One sweep. Fills @p pi for every agent a live worker answered for and
135 * clears its @p pending flag; the rest are left to the caller.
136 */
137 void sweep(const std::vector<double>& x,
138 std::vector<std::vector<double>>& pi,
139 std::vector<bool>& pending) {
140 for (std::size_t w = 0; w < endpoints_.size(); ++w) {
141 if (!live_[w] || owns_[w].empty()) continue;
142 try {
143 nlohmann::json msg;
144 msg["op"] = "sweep";
145 msg["x"] = x;
146 const nlohmann::json reply = call(w, msg);
147 if (reply.value("op", std::string()) != "swept") {
148 throw std::runtime_error("unexpected reply '" +
149 reply.value("op", std::string()) + "'");
150 }
151 for (const nlohmann::json& e : reply.at("agents")) {
152 const std::size_t k = e.at("k").get<std::size_t>();
153 if (k >= pi.size()) {
154 throw std::runtime_error("worker answered for an agent out of range");
155 }
156 pi[k] = e.at("pi").get<std::vector<double>>();
157 pending[k] = false;
158 }
159 } catch (const std::exception& e) {
160 std::cerr << "[LINE] Warning: AG worker " << endpoints_[w]
161 << " failed mid-sweep (" << e.what()
162 << "); its agents are solved on the coordinator for the rest of "
163 << "the run" << std::endl;
164 live_[w] = false;
165 close_one(w);
166 }
167 }
168 }
169
170 private:
171 /**
172 * Round-robin in agent index order, computed before any connection is
173 * attempted so a dead worker does not shift the others' agents. A pure
174 * function of (agent count, worker count), so a rerun assigns the same
175 * agents to the same workers.
176 */
177 void partition(std::size_t n) {
178 owns_.assign(endpoints_.size(), std::vector<std::size_t>());
179 for (std::size_t k = 0; k < n; ++k) owns_[k % endpoints_.size()].push_back(k);
180 }
181
182 static nlohmann::json encode(const AgWirePayload& a) {
183 nlohmann::json j;
184 j["k"] = a.k;
185 j["n"] = a.n;
186 j["mph"] = a.mph;
187 j["nlev"] = a.nlev;
188 j["level"] = a.level;
189 j["L"] = a.L;
190 nlohmann::json passive = nlohmann::json::array();
191 for (std::size_t i = 0; i < a.passive_c.size(); ++i) {
192 nlohmann::json e;
193 e["c"] = a.passive_c[i];
194 e["M"] = a.passive_m[i];
195 passive.push_back(e);
196 }
197 nlohmann::json active = nlohmann::json::array();
198 for (std::size_t i = 0; i < a.active_c.size(); ++i) {
199 nlohmann::json e;
200 e["c"] = a.active_c[i];
201 e["M"] = a.active_m[i];
202 active.push_back(e);
203 }
204 j["passive"] = passive;
205 j["active"] = active;
206 return j;
207 }
208
209 void connect(std::size_t w) {
210 const std::string& ep = endpoints_[w];
211 const std::size_t colon = ep.rfind(':');
212 if (colon == std::string::npos || colon == 0) {
213 throw std::runtime_error("malformed endpoint '" + ep + "', expected host:port");
214 }
215 const std::string host = ep.substr(0, colon);
216 const std::string port = ep.substr(colon + 1);
217
218 addrinfo hints{};
219 hints.ai_family = AF_UNSPEC;
220 hints.ai_socktype = SOCK_STREAM;
221 addrinfo* res = nullptr;
222 if (::getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || res == nullptr) {
223 throw std::runtime_error("cannot resolve " + ep);
224 }
225 int fd = -1;
226 for (addrinfo* p = res; p != nullptr; p = p->ai_next) {
227 fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
228 if (fd < 0) continue;
229 timeval tv{};
230 tv.tv_sec = static_cast<long>(timeout_);
231 tv.tv_usec = static_cast<long>((timeout_ - tv.tv_sec) * 1e6);
232 ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
233 ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
234 if (::connect(fd, p->ai_addr, p->ai_addrlen) == 0) break;
235 ::close(fd);
236 fd = -1;
237 }
238 ::freeaddrinfo(res);
239 if (fd < 0) throw std::runtime_error("cannot connect to " + ep);
240 fds_[w] = fd;
241 }
242
243 nlohmann::json call(std::size_t w, const nlohmann::json& msg) {
244 const std::string line = msg.dump() + "\n";
245 std::size_t sent = 0;
246 while (sent < line.size()) {
247 const ssize_t n = ::send(fds_[w], line.data() + sent, line.size() - sent, 0);
248 if (n <= 0) throw std::runtime_error("send failed: " + std::string(std::strerror(errno)));
249 sent += static_cast<std::size_t>(n);
250 }
251 std::string reply;
252 char ch = 0;
253 while (true) {
254 const ssize_t n = ::recv(fds_[w], &ch, 1, 0);
255 if (n <= 0) throw std::runtime_error("worker closed the connection");
256 if (ch == '\n') break;
257 reply.push_back(ch);
258 }
259 nlohmann::json j = nlohmann::json::parse(reply);
260 if (j.contains("error")) throw std::runtime_error(j.at("error").get<std::string>());
261 return j;
262 }
263
264 void close_one(std::size_t w) {
265 if (fds_[w] >= 0) {
266 ::close(fds_[w]);
267 fds_[w] = -1;
268 }
269 }
270
271 void close_all() {
272 for (std::size_t w = 0; w < fds_.size(); ++w) {
273 if (fds_[w] < 0) continue;
274 const std::string bye = "{\"op\":\"bye\"}\n";
275 ::send(fds_[w], bye.data(), bye.size(), 0);
276 close_one(w);
277 }
278 }
279
280 std::vector<std::string> endpoints_;
281 double timeout_;
282 std::vector<int> fds_;
283 std::vector<bool> live_;
284 std::vector<std::vector<std::size_t>> owns_;
285 bool assigned_ = false;
286};
287
288} // namespace ag
289} // namespace line
290
291#endif // LINE_SOLVERS_AG_AG_WORKER_CLIENT_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
void ensure_assigned(std::size_t num_agents, Payload payload)
Connect and ship the static half of each owned agent, once.
AgWorkerPool(const std::vector< std::string > &endpoints, double timeout_seconds)
void sweep(const std::vector< double > &x, std::vector< std::vector< double > > &pi, std::vector< bool > &pending)
One sweep.
AgWorkerPool(const AgWorkerPool &)=delete
AgWorkerPool & operator=(const AgWorkerPool &)=delete
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< std::array< double, 3 > > ag_triplets(const Matrix< double > &m)
Non-zero entries of M as [row, col, value] triplets, 0-based.
One agent's static description, in the wire form the worker expects.
std::vector< int > passive_c
std::vector< int > active_c
std::vector< std::array< double, 3 > > L
[row, col, value] triplets, 0-based.
std::vector< int > level
std::vector< std::vector< std::array< double, 3 > > > active_m
std::vector< std::vector< std::array< double, 3 > > > passive_m