ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
kcmcp_client.cpp
Go to the documentation of this file.
1/**
2 * @file kcmcp_client.cpp
3 * @brief Implementation of the in-extension KCMCP client (see kcmcp_client.h).
4 */
5extern "C" {
6#include "postgres.h"
7#include "miscadmin.h"
8#include "storage/ipc.h" /* on_proc_exit */
9
10#include <sys/socket.h>
11#include <sys/un.h>
12#include <netdb.h>
13#include <poll.h>
14#include <unistd.h>
15#include <string.h>
16#include <errno.h>
17}
18
19// PostgreSQL's elog.h defines ERROR (and other log levels) as macros, which
20// would clobber the kcmcp::Type::ERROR enumerator below. We use the provsql
21// error macros, not bare elog levels, in this file, so dropping ERROR is safe.
22#undef ERROR
23
24#include "kcmcp_client.h"
25#include "kcmcp_protocol.h"
26#include "provsql_config.h"
27
28#include <stdexcept>
29#include <string>
30
31using namespace kcmcp;
32
33namespace {
34
35// Largest RESULT (compiled d-DNNF) we will accept from the server.
36constexpr uint32_t CLIENT_RECV_MAX = 256u * 1024 * 1024;
37// Split our outbound problem into MORE frames at the 1 MiB interoperability
38// floor, so any conformant server accepts it without advertising a larger
39// max_payload (which we do not parse from its HELLO).
40constexpr uint32_t CLIENT_SEND_MAX = 1u * 1024 * 1024;
41
42// Connect to "unix:/path" or "host:port"; returns a connected fd or -1.
43int connect_endpoint(const std::string &endpoint)
44{
45#ifdef PROVSQL_NO_SUBPROCESS
46 /* No sockets in the WASM sandbox: report "no connection" so the KCMCP
47 path falls back to the CLI compilers (also absent) and ultimately to
48 the in-process tree-decomposition compiler. A remote KCMCP-over-
49 WebSocket transport is a separate, opt-in addition. */
50 (void) endpoint;
51 return -1;
52#else
53 if (endpoint.rfind("unix:", 0) == 0) {
54 std::string path = endpoint.substr(5);
55 int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
56 if (fd < 0)
57 return -1;
58 struct sockaddr_un addr;
59 memset(&addr, 0, sizeof(addr));
60 addr.sun_family = AF_UNIX;
61 if (path.size() >= sizeof(addr.sun_path)) {
62 ::close(fd);
63 return -1;
64 }
65 strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1);
66 if (::connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
67 ::close(fd);
68 return -1;
69 }
70 return fd;
71 }
72
73 auto colon = endpoint.rfind(':');
74 if (colon == std::string::npos)
75 return -1;
76 std::string host = endpoint.substr(0, colon), port = endpoint.substr(colon + 1);
77 struct addrinfo hints, *res = nullptr;
78 memset(&hints, 0, sizeof(hints));
79 hints.ai_family = AF_UNSPEC;
80 hints.ai_socktype = SOCK_STREAM;
81 if (::getaddrinfo(host.c_str(), port.c_str(), &hints, &res) != 0 || !res)
82 return -1;
83 int fd = -1;
84 for (auto *ai = res; ai; ai = ai->ai_next) {
85 fd = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
86 if (fd < 0)
87 continue;
88 if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0)
89 break;
90 ::close(fd);
91 fd = -1;
92 }
93 freeaddrinfo(res);
94 return fd;
95#endif /* PROVSQL_NO_SUBPROCESS */
96}
97
98uint16_t get_u16(const std::string &s, size_t off)
99{
100 return (uint16_t(static_cast<unsigned char>(s[off])) << 8)
101 | uint16_t(static_cast<unsigned char>(s[off + 1]));
102}
103
104// A job-level ERROR frame from the server (codes 1-6): a valid response on a
105// healthy, synchronised connection, distinct from an I/O / protocol failure --
106// so the caller propagates it without dropping or retrying the connection.
107struct ServerError : std::runtime_error {
108 using std::runtime_error::runtime_error;
109};
110
111// --- Per-backend cached connection ---------------------------------------
112// KCMCP mandates one connection for the session's life so the server's warm
113// cross-query cache is not discarded; today it also saves the per-compile
114// connect + HELLO round-trip. A backend is single-threaded and compiles one
115// circuit at a time, so a single cached connection (not a pool) suffices.
116int g_fd = -1; // cached connection fd, or -1 when none
117std::string g_endpoint; // endpoint g_fd is connected to
118uint32_t g_request_id = 0; // monotonically increasing REQUEST id
119bool g_atexit_registered = false;
120
121void close_cached()
122{
123 if (g_fd >= 0)
124 ::close(g_fd);
125 g_fd = -1;
126 g_endpoint.clear();
127}
128
129// on_proc_exit hook: gracefully BYE and close the cached connection at backend
130// exit. Best-effort and must not throw (it runs during shutdown); the OS would
131// close the fd regardless, this just lets the server release the session early.
132void kcmcp_atexit(int code, Datum arg)
133{
134 (void) code;
135 (void) arg;
136 if (g_fd >= 0) {
137 unsigned char bye[10] = { 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // BYE, no payload
138 ssize_t n = ::write(g_fd, bye, sizeof(bye));
139 (void) n;
140 ::close(g_fd);
141 g_fd = -1;
142 }
143}
144
145// Block until the cached socket is readable, servicing PostgreSQL cancel /
146// terminate while we wait. A longjmp out of CHECK_FOR_INTERRUPTS() skips C++
147// destructors, so -- exactly as run_in_own_pgroup does -- we detect a pending
148// cancel ourselves, close the connection first (the server sees EOF and
149// abandons the job, and the cache is left clean for the next statement), then
150// let CHECK_FOR_INTERRUPTS() raise it.
151void wait_readable_or_cancel()
152{
153 for (;;) {
154 struct pollfd pfd;
155 pfd.fd = g_fd;
156 pfd.events = POLLIN;
157 pfd.revents = 0;
158 int r = ::poll(&pfd, 1, 100);
159 if (r > 0 && (pfd.revents & (POLLIN | POLLHUP | POLLERR)))
160 return;
161 if (r < 0 && errno != EINTR)
162 return; // let the subsequent recv surface the error
163 if (QueryCancelPending || ProcDiePending) {
164 close_cached();
165 CHECK_FOR_INTERRUPTS(); // raises; connection already dropped
166 return; // unreached if it raised
167 }
168 }
169}
170
171// Ensure g_fd is a handshaken connection to @p endpoint, reusing the cached one
172// when it matches. Throws (and leaves g_fd == -1) if it cannot connect or
173// handshake.
174void ensure_connection(const std::string &endpoint)
175{
176 if (g_fd >= 0 && g_endpoint == endpoint)
177 return; // reuse the warm connection
178 close_cached();
179
180 int fd = connect_endpoint(endpoint);
181 if (fd < 0)
182 throw std::runtime_error("cannot connect to KCMCP endpoint '" + endpoint + "'");
183 try {
184 Connection conn(fd, CLIENT_RECV_MAX, CLIENT_SEND_MAX);
185 conn.send(Type::HELLO, 0, "{\"kcmcp\":[1,0],\"client\":\"ProvSQL\"}");
186 Message m;
187 if (!conn.recv(m))
188 throw std::runtime_error("KCMCP server closed during handshake");
189 if (m.type == Type::ERROR)
190 throw std::runtime_error("KCMCP handshake refused: "
191 + (m.payload.size() > 2 ? m.payload.substr(2) : ""));
192 if (m.type != Type::HELLO)
193 throw std::runtime_error("KCMCP: expected HELLO from server");
194 } catch (...) {
195 ::close(fd);
196 throw;
197 }
198 g_fd = fd;
199 g_endpoint = endpoint;
200}
201
202// Issue one compile REQUEST on the cached connection and return the d-DNNF.
203std::string do_compile(uint8_t input_format, const std::string &problem)
204{
205 Connection conn(g_fd, CLIENT_RECV_MAX, CLIENT_SEND_MAX);
206
207 std::string req;
208 req.push_back(static_cast<char>(2)); // operation: compile
209 req.push_back(static_cast<char>(input_format)); // 0 dimacs-cnf / 1 circuit-bcs12
210 req.push_back(static_cast<char>(4)); // output_format: ddnnf-nnf
211 req.push_back(0); // reserved
212 req.push_back(0); // options_len hi
213 req.push_back(0); // options_len lo
214 req += problem;
215 conn.send(Type::REQUEST, ++g_request_id, req);
216
217 // Read frames until the RESULT, skipping PROGRESS heartbeats; honour
218 // cancel/timeout while the server computes.
219 Message m;
220 for (;;) {
221 wait_readable_or_cancel();
222 if (!conn.recv(m))
223 throw std::runtime_error("KCMCP server closed before RESULT");
224 if (m.type == Type::PROGRESS)
225 continue;
226 if (m.type == Type::ERROR) {
227 uint16_t code = m.payload.size() >= 2 ? get_u16(m.payload, 0) : 0;
228 std::string msg = m.payload.size() > 2 ? m.payload.substr(2) : "";
229 throw ServerError("KCMCP server error " + std::to_string(code)
230 + ": " + msg);
231 }
232 if (m.type == Type::RESULT)
233 break;
234 throw std::runtime_error("KCMCP: unexpected frame type in reply");
235 }
236
237 // RESULT payload: result_format u8, reserved u8, meta_len u16, meta, result.
238 if (m.payload.size() < 4)
239 throw std::runtime_error("KCMCP: truncated RESULT");
240 if (static_cast<unsigned char>(m.payload[0]) != 4)
241 throw std::runtime_error("KCMCP: server returned a non-ddnnf-nnf result");
242 uint16_t meta_len = get_u16(m.payload, 2);
243 if (4u + meta_len > m.payload.size())
244 throw std::runtime_error("KCMCP: malformed RESULT meta");
245 return m.payload.substr(4 + meta_len);
246}
247
248} // namespace
249
250namespace provsql {
251
252std::string kcmcp_compile(const std::string &endpoint, uint8_t input_format,
253 const std::string &problem)
254{
255 // SIGPIPE would otherwise kill the backend if the server vanishes mid-send.
256 ::signal(SIGPIPE, SIG_IGN);
257 if (!g_atexit_registered) {
258 on_proc_exit(kcmcp_atexit, (Datum) 0);
259 g_atexit_registered = true;
260 }
261
262 // Use the cached connection; if a *reused* one fails (server respawned or an
263 // idle link dropped), reconnect once on a fresh connection and retry. A
264 // failure on a connection we just opened means the server is unreachable, so
265 // we give up (the caller falls back to the CLI path). A server ERROR frame
266 // is a healthy-connection response, so it is propagated without a retry.
267 for (int attempt = 0; ; ++attempt) {
268 bool reusing = (g_fd >= 0 && g_endpoint == endpoint);
269 try {
270 ensure_connection(endpoint);
271 return do_compile(input_format, problem);
272 } catch (const ServerError &) {
273 throw;
274 } catch (const std::exception &) {
275 close_cached();
276 if (reusing && attempt == 0)
277 continue;
278 throw;
279 }
280 }
281}
282
283} // namespace provsql
Framed message transport over one connected socket fd.
In-extension KCMCP client: compile a Boolean problem on a warm, socket-attached knowledge compiler in...
Wire codec for KCMCP, the Knowledge Compiler / Model Counter Protocol (see doc/source/dev/kc-server-p...
std::string kcmcp_compile(const std::string &endpoint, uint8_t input_format, const std::string &problem)
Compile problem on a KCMCP server and return its d-DNNF NNF text.
Build-configuration switches shared across the C and C++ sources.
A fully reassembled inbound message (MORE frames concatenated).
std::string payload