Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
http_client.hpp
Go to the documentation of this file.
1/*
2 * The MIT License (MIT)
3 *
4 * Copyright (c) 2025 Yaroslav Riabtsev <yaroslav.riabtsev@rwth-aachen.de>
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included
14 * in all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25#ifndef ARACHNE_HTTP_CLIENT_HPP
26#define ARACHNE_HTTP_CLIENT_HPP
27
28#include "utils.hpp"
29
30#include <chrono>
31#include <mutex>
32
33namespace corespace {
34/**
35 * @class http_client
36 * @brief Minimal, synchronous HTTP GET client built on libcurl.
37 *
38 * Responsibilities:
39 * - Build request URLs with encoded query parameters.
40 * - Issue HTTP GET requests with redirect following enabled.
41 * - Apply bounded exponential backoff with jitter for retryable outcomes:
42 * network errors, 408 (Request Timeout), 429 (Too Many Requests), and 5xx.
43 * - Aggregate lightweight, thread-safe network metrics.
44 *
45 * Lifetime and thread-safety:
46 * - A single easy handle (`CURL*`) is owned by the instance and reused
47 * across requests; therefore an `http_client` object is not thread-safe.
48 * Use one instance per calling thread.
49 * - `curl_global_init` is performed once per process via `std::call_once`.
50 */
51class http_client final {
52public:
53 /**
54 * @brief Construct a client and initialize libcurl.
55 *
56 * Effects:
57 * - Ensures `curl_global_init` is called exactly once process-wide.
58 * - Creates an easy handle and installs default options: user agent,
59 * `Accept` header, redirect following, transparent decoding,
60 * timeouts, and signal suppression.
61 *
62 * @throws std::runtime_error if libcurl initialization fails or
63 * header allocation fails.
64 */
65 explicit http_client();
66
67 /**
68 * @brief Perform an HTTP GET to @p url with optional query @p params.
69 *
70 * Behavior:
71 * - Builds a `CURLU` URL with `params` URL-encoded and appended.
72 * - Executes the request; on non-2xx or transport errors, applies the
73 * retry policy up to `opt.max_retries` with jittered backoff and
74 * an optional server `Retry-After` hint.
75 * - On success (2xx + `CURLE_OK`) returns the populated response.
76 *
77 * Failure:
78 * - If all attempts fail with a libcurl error, throws
79 * `std::runtime_error("curl error: ...")`.
80 * - If all attempts return non-success HTTP codes, throws
81 * `std::runtime_error("http error: <status>")`.
82 *
83 * Metrics:
84 * - Updates `metrics` after each attempt (including retries).
85 *
86 * @param url Absolute or base URL.
87 * @param params Optional list of query parameters to append.
88 * @param accept Optional Accept header value to override the default;
89 * empty string uses the client's configured accept header.
90 * @param timeout_sec Optional per-request timeout in seconds; if negative
91 * the client's default timeout is used.
92 * @return http_response on success (2xx).
93 * @throws std::runtime_error on terminal failure as described above.
94 */
96 get(std::string_view url, const parameter_list& params = {},
97 std::string_view accept = {}, int timeout_sec = -1);
98 /**
99 * @brief Perform an HTTP POST with form-encoded body.
100 *
101 * Builds a URL from @p url and @p query, serializes @p form as
102 * application/x-www-form-urlencoded and posts it. Retry behaviour and
103 * metrics follow the same semantics as get().
104 *
105 * @param url Endpoint URL.
106 * @param form Form key/value pairs to serialize in the body.
107 * @param query Optional query parameters appended to the URL.
108 * @param accept Optional Accept header override.
109 * @param timeout_sec Optional per-request timeout in seconds; negative to
110 * use default.
111 * @return http_response on success (2xx).
112 * @throws std::runtime_error on terminal failure.
113 */
115 std::string_view url, const parameter_list& form,
116 const parameter_list& query = {}, std::string_view accept = {},
117 int timeout_sec = -1
118 );
119 /**
120 * @brief Perform an HTTP POST with a raw body.
121 *
122 * Builds a URL from @p url and @p query and posts the raw @p body with
123 * Content-Type set to @p content_type. Retry behaviour and metrics follow
124 * the same semantics as get().
125 *
126 * @param url Endpoint URL.
127 * @param body Raw request body to send.
128 * @param content_type Content-Type header value for the body.
129 * @param query Optional query parameters appended to the URL.
130 * @param accept Optional Accept header override.
131 * @param timeout_sec Optional per-request timeout in seconds; negative to
132 * use default.
133 * @return http_response on success (2xx).
134 * @throws std::runtime_error on terminal failure.
135 */
137 std::string_view url, std::string_view body,
138 std::string_view content_type, const parameter_list& query = {},
139 std::string_view accept = {}, int timeout_sec = -1
140 );
141
142 /**
143 * @brief Access aggregated network metrics.
144 * @return Const reference to the metrics snapshot.
145 */
146 [[nodiscard]] const network_metrics& metrics_info() const;
147
148private:
149 /// Unique pointer type for `CURLU` with proper deleter.
151 /**
152 * @brief Construct a `CURLU` handle from @p url and append @p params.
153 *
154 * Each parameter is URL-encoded and appended via `CURLU_APPENDQUERY`.
155 *
156 * @param url Base URL.
157 * @param params Query parameters.
158 * @return Owning smart pointer to a configured `CURLU` handle.
159 * @throws std::runtime_error if allocation or URL assembly fails.
160 */
161 static curl_url_ptr
162 build_url(std::string_view url, const parameter_list& params);
163 /**
164 * @brief Execute a single HTTP GET using the prepared URL handle.
165 *
166 * Side effects:
167 * - Installs write callback to accumulate the response body.
168 * - Measures elapsed steady-clock time and returns it via @p elapsed.
169 * - Reads HTTP status and headers after the transfer.
170 *
171 * @param url_handle Prepared `CURLU` handle (owned by caller).
172 * @param elapsed Out: time spent in `curl_easy_perform`.
173 * @param accept Optional Accept header override; empty means use client
174 * default.
175 * @param timeout_sec Optional per-request timeout in seconds; negative to
176 * use client default.
177 * @return Populated `http_response` (may carry a libcurl error).
178 */
180 CURLU* url_handle, std::chrono::milliseconds& elapsed,
181 std::string_view accept = {}, int timeout_sec = -1
182 ) const;
183
184 /**
185 * @brief Execute a single HTTP POST with given body and content type.
186 *
187 * Sets temporary headers (Content-Type and Accept), posts the body,
188 * measures elapsed time in @p elapsed, reads status and headers, and
189 * records any libcurl error message.
190 *
191 * @param url_handle Prepared `CURLU` handle (owned by caller).
192 * @param elapsed Out: time spent in `curl_easy_perform`.
193 * @param content_type Content-Type header value.
194 * @param body Body bytes to send.
195 * @param accept Optional Accept header override; empty means use
196 * client default.
197 * @param timeout_sec Optional per-request timeout in seconds; negative to
198 * use client default.
199 * @return Populated `http_response` (may carry a libcurl error).
200 */
202 CURLU* url_handle, std::chrono::milliseconds& elapsed,
203 std::string_view content_type, std::string_view body,
204 std::string_view accept = {}, int timeout_sec = -1
205 ) const;
206 std::string build_form_body(const parameter_list& form) const;
207
208 /**
209 * @brief Refresh the header multimap from the last transfer.
210 *
211 * Enumerates headers via `curl_easy_nextheader` and fills
212 * `response.header`.
213 *
214 * @param response Response object to update.
215 */
216 void update_headers(http_response& response) const;
217 /**
218 * @brief Update counters and histograms after an attempt.
219 *
220 * Increments `requests`, accumulates `network_ms`, bumps status
221 * histogram (if within bounds), and adds to `bytes_received`.
222 *
223 * @param response Result of the attempt.
224 * @param elapsed Duration spent in libcurl during the attempt.
225 */
226 void update_metrics(
227 const http_response& response, std::chrono::milliseconds elapsed
228 );
229 /**
230 * @brief Success predicate: transport OK and HTTP 2xx.
231 * @param response Response to check.
232 * @return true if `CURLE_OK` and 200 <= status < 300.
233 */
234 [[nodiscard]] static bool status_good(const http_response& response);
235 /**
236 * @brief Retry predicate for transient outcomes.
237 *
238 * Retries on:
239 * - any libcurl error (i.e., `!net_ok`),
240 * - HTTP 408 (Request Timeout),
241 * - HTTP 429 (Too Many Requests),
242 * - HTTP 5xx.
243 *
244 * @param response Response to inspect.
245 * @param net_ok Whether the transport completed without libcurl error.
246 * @return true if another attempt should be made.
247 */
248 [[nodiscard]] static bool
249 status_retry(const http_response& response, bool net_ok);
250 /**
251 * @brief Compute the next backoff delay for @p attempt (1-based).
252 *
253 * Strategy: exponential backoff with full jitter. The base grows as
254 * `retry_base_ms * 2^(attempt-1)` and a uniform random component in
255 * `[0, base]` is added; the result is capped at `retry_max_ms`.
256 *
257 * @param attempt Attempt number starting from 1.
258 * @return Milliseconds to sleep before the next attempt.
259 */
260 [[nodiscard]] long long next_delay(int attempt) const;
261 /**
262 * @brief Apply server-provided retry hint if present.
263 *
264 * If `CURLINFO_RETRY_AFTER` yields a non-negative value, interpret it
265 * as seconds and raise @p sleep_ms to at least that many milliseconds.
266 *
267 * @param sleep_ms In/out: proposed client backoff in milliseconds.
268 */
269 void apply_server_retry_hint(long long& sleep_ms) const;
270
271 /**
272 * @brief libcurl write callback: append chunk to response body.
273 * @param ptr Pointer to received data.
274 * @param size Element size.
275 * @param n Number of elements.
276 * @param data `std::string*` accumulator (response body).
277 * @return Number of bytes consumed (size * n).
278 */
279 static size_t
280 write_callback(const char* ptr, size_t size, size_t n, void* data);
281
282 const network_options opt {}; ///< Fixed options installed at construction.
283 network_metrics metrics; ///< Aggregated metrics (atomic counters).
284 mutable std::mutex mu;
286 nullptr, &curl_easy_cleanup
287 }; ///< Reused easy handle (not thread-safe).
289 nullptr, &curl_slist_free_all
290 }; ///< Owned request header list.
291};
292}
293#endif // ARACHNE_HTTP_CLIENT_HPP
Accumulates entity IDs into per-kind batches and organizes groups.
Definition arachne.hpp:47
std::unordered_map< std::string, int > candidates
Definition arachne.hpp:280
std::array< std::unordered_set< std::string >, batched_kind_count > extra_batches
Definition arachne.hpp:273
bool touch_entity(const std::string &id_with_prefix) noexcept
Increment the touch counter for a single full ID (prefix REQUIRED).
Definition arachne.cpp:224
static std::string entity_root(const std::string &id)
Extract the lexeme root from a full ID string.
Definition arachne.cpp:74
std::string current_group
Definition arachne.hpp:290
int touch_ids(std::span< const int > ids, corespace::entity_kind kind)
Batch variant of touch for numeric IDs.
Definition arachne.cpp:59
static bool parse_id(const std::string &entity, size_t &pos, int &id)
Parse a full ID string and extract the numeric portion.
Definition arachne.cpp:149
bool new_group(std::string name="")
Create or select a group and make it current.
Definition arachne.cpp:31
size_t add_entity(const std::string &id_with_prefix, bool force=false, std::string name="")
Enqueue a full (prefixed) ID string and add it to a group.
Definition arachne.cpp:235
std::unordered_map< std::string, std::unordered_set< std::string > > groups
Definition arachne.hpp:277
std::chrono::milliseconds staleness_threshold
Definition arachne.hpp:291
bool enqueue(std::string_view id, corespace::entity_kind kind, bool interactive) const
Decide whether an entity should be enqueued for fetching.
Definition arachne.cpp:201
const size_t batch_threshold
Typical unauthenticated entity-per-request cap.
Definition arachne.hpp:284
pheidippides phe_client
Definition arachne.hpp:293
const int candidates_threshold
Intentional high bar for curiosity-driven candidates.
Definition arachne.hpp:286
static std::string normalize(int id, corespace::entity_kind kind)
Normalize a numeric ID with the given kind to a prefixed string.
Definition arachne.cpp:165
static bool ask_update(std::string_view id, corespace::entity_kind kind, std::chrono::milliseconds age)
Placeholder for interactive staleness confirmation.
Definition arachne.cpp:194
void select_group(std::string name)
Select an existing group or create it on demand.
Definition arachne.cpp:184
std::array< std::unordered_set< std::string >, batched_kind_count > main_batches
Definition arachne.hpp:271
int queue_size(corespace::entity_kind kind) const noexcept
Get the number of queued (pending) entities tracked in the main batch containers.
Definition arachne.cpp:107
corespace::interface ui
Definition arachne.hpp:292
static corespace::entity_kind identify(const std::string &entity) noexcept
Determine the kind of a full ID string.
Definition arachne.cpp:122
bool flush(corespace::entity_kind kind=corespace::entity_kind::any)
Flush (send) up to batch_threshold entities of a specific kind.
Definition arachne.cpp:99
size_t add_ids(std::span< const int > ids, corespace::entity_kind kind, std::string name="")
Enqueue numeric IDs with a given kind and add them to a group.
Definition arachne.cpp:42
Batch courier for Wikidata/Commons: collects IDs, issues HTTP requests, and returns a merged JSON pay...
corespace::call_preview preview(const corespace::sparql_request &request) const
Produce a call preview describing the HTTP request that would be made.
corespace::http_client client
Reused HTTP client (not thread-safe across threads).
nlohmann::json wdqs(std::string query)
Convenience wrapper to run a raw SPARQL query string.
corespace::call_preview build_call_preview(const corespace::sparql_request &request) const
corespace::wdqs_options wdqs_opt
nlohmann::json sparql(const corespace::sparql_request &request)
Execute a SPARQL query according to the provided request.
static std::string join_str(std::span< const std::string > ids, std::string_view separator="|")
Join a span of strings with a separator (no encoding or validation).
const corespace::network_metrics & metrics_info() const
Access aggregated network metrics of the underlying client.
nlohmann::json fetch_json(const std::unordered_set< std::string > &batch, corespace::entity_kind kind=corespace::entity_kind::any)
Fetch metadata for a set of entity IDs and return a merged JSON object.
corespace::options opt
Request shaping parameters (chunking, fields, base params).
static bool status_retry(const http_response &response, bool net_ok)
Retry predicate for transient outcomes.
std::unique_ptr< curl_slist, decltype(&curl_slist_free_all)> header_list
Owned request header list.
void update_headers(http_response &response) const
Refresh the header multimap from the last transfer.
http_response request_get(CURLU *url_handle, std::chrono::milliseconds &elapsed, std::string_view accept={}, int timeout_sec=-1) const
Execute a single HTTP GET using the prepared URL handle.
http_client()
Construct a client and initialize libcurl.
const network_metrics & metrics_info() const
Access aggregated network metrics.
network_metrics metrics
Aggregated metrics (atomic counters).
http_response post_raw(std::string_view url, std::string_view body, std::string_view content_type, const parameter_list &query={}, std::string_view accept={}, int timeout_sec=-1)
Perform an HTTP POST with a raw body.
long long next_delay(int attempt) const
Compute the next backoff delay for attempt (1-based).
const network_options opt
Fixed options installed at construction.
static curl_url_ptr build_url(std::string_view url, const parameter_list &params)
Construct a CURLU handle from url and append params.
http_response get(std::string_view url, const parameter_list &params={}, std::string_view accept={}, int timeout_sec=-1)
Perform an HTTP GET to url with optional query params.
http_response request_post(CURLU *url_handle, std::chrono::milliseconds &elapsed, std::string_view content_type, std::string_view body, std::string_view accept={}, int timeout_sec=-1) const
Execute a single HTTP POST with given body and content type.
static bool status_good(const http_response &response)
Success predicate: transport OK and HTTP 2xx.
http_response post_form(std::string_view url, const parameter_list &form, const parameter_list &query={}, std::string_view accept={}, int timeout_sec=-1)
Perform an HTTP POST with form-encoded body.
void apply_server_retry_hint(long long &sleep_ms) const
Apply server-provided retry hint if present.
std::unique_ptr< CURLU, decltype(&curl_url_cleanup)> curl_url_ptr
Unique pointer type for CURLU with proper deleter.
std::string build_form_body(const parameter_list &form) const
void update_metrics(const http_response &response, std::chrono::milliseconds elapsed)
Update counters and histograms after an attempt.
std::unique_ptr< CURL, decltype(&curl_easy_cleanup)> curl
Reused easy handle (not thread-safe).
static size_t write_callback(const char *ptr, size_t size, size_t n, void *data)
libcurl write callback: append chunk to response body.
static constexpr std::string prefixes
Definition arachne.cpp:29
constexpr std::size_t batched_kind_count
Number of batchable kinds (Q, P, L, M, E, form, sense).
Definition arachne.hpp:33
entity_kind
Wikidata entity kind.
Definition utils.hpp:47
@ any
API selector (e.g., flush(any)); not directly batchable.
Definition utils.hpp:55
@ lexeme
IDs prefixed with 'L'.
Definition utils.hpp:50
@ form
Lexeme form IDs such as "L<lexeme>-F<form>".
Definition utils.hpp:53
@ unknown
Unrecognized/invalid identifier.
Definition utils.hpp:56
@ sense
Lexeme sense IDs such as "L<lexeme>-S<sense>".
Definition utils.hpp:54
std::string random_hex(std::size_t n)
Return exactly n random hexadecimal characters (lowercase).
Definition rng.cpp:33
Result object for an HTTP transfer.
Definition utils.hpp:157
Fixed runtime options for the HTTP client.
Definition utils.hpp:183
Configuration for fetching entities via MediaWiki/Wikibase API.
Definition utils.hpp:87
Options specific to WDQS usage and heuristics.
Definition utils.hpp:249