1 /** @file
2
3 Implementation of nexthop consistent hash selections strategies.
4
5 @section license License
6
7 Licensed to the Apache Software Foundation (ASF) under one
8 or more contributor license agreements. See the NOTICE file
9 distributed with this work for additional information
10 regarding copyright ownership. The ASF licenses this file
11 to you under the Apache License, Version 2.0 (the
12 "License"); you may not use this file except in compliance
13 with the License. You may obtain a copy of the License at
14
15 http://www.apache.org/licenses/LICENSE-2.0
16
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
22 */
23
24 #include <yaml-cpp/yaml.h>
25
26 #include "tscore/HashSip.h"
27 #include "HttpSM.h"
28 #include "NextHopConsistentHash.h"
29
30 // hash_key strings.
31 constexpr std::string_view hash_key_url = "url";
32 constexpr std::string_view hash_key_hostname = "hostname";
33 constexpr std::string_view hash_key_path = "path";
34 constexpr std::string_view hash_key_path_query = "path+query";
35 constexpr std::string_view hash_key_path_fragment = "path+fragment";
36 constexpr std::string_view hash_key_cache = "cache_key";
37
38 static HostRecord *
chash_lookup(const std::shared_ptr<ATSConsistentHash> & ring,uint64_t hash_key,ATSConsistentHashIter * iter,bool * wrapped,ATSHash64Sip24 * hash,bool * hash_init,bool * mapWrapped,uint64_t sm_id)39 chash_lookup(const std::shared_ptr<ATSConsistentHash> &ring, uint64_t hash_key, ATSConsistentHashIter *iter, bool *wrapped,
40 ATSHash64Sip24 *hash, bool *hash_init, bool *mapWrapped, uint64_t sm_id)
41 {
42 HostRecord *host_rec = nullptr;
43
44 if (*hash_init == false) {
45 host_rec = static_cast<HostRecord *>(ring->lookup_by_hashval(hash_key, iter, wrapped));
46 *hash_init = true;
47 } else {
48 host_rec = static_cast<HostRecord *>(ring->lookup(nullptr, iter, wrapped, hash));
49 }
50 bool wrap_around = *wrapped;
51 *wrapped = (*mapWrapped && *wrapped) ? true : false;
52 if (!*mapWrapped && wrap_around) {
53 *mapWrapped = true;
54 }
55
56 return host_rec;
57 }
58
~NextHopConsistentHash()59 NextHopConsistentHash::~NextHopConsistentHash()
60 {
61 NH_Debug(NH_DEBUG_TAG, "destructor called for strategy named: %s", strategy_name.c_str());
62 }
63
64 bool
Init(const YAML::Node & n)65 NextHopConsistentHash::Init(const YAML::Node &n)
66 {
67 ATSHash64Sip24 hash;
68
69 try {
70 if (n["hash_key"]) {
71 auto hash_key_val = n["hash_key"].Scalar();
72 if (hash_key_val == hash_key_url) {
73 hash_key = NH_URL_HASH_KEY;
74 } else if (hash_key_val == hash_key_hostname) {
75 hash_key = NH_HOSTNAME_HASH_KEY;
76 } else if (hash_key_val == hash_key_path) {
77 hash_key = NH_PATH_HASH_KEY;
78 } else if (hash_key_val == hash_key_path_query) {
79 hash_key = NH_PATH_QUERY_HASH_KEY;
80 } else if (hash_key_val == hash_key_path_fragment) {
81 hash_key = NH_PATH_FRAGMENT_HASH_KEY;
82 } else if (hash_key_val == hash_key_cache) {
83 hash_key = NH_CACHE_HASH_KEY;
84 } else {
85 hash_key = NH_PATH_HASH_KEY;
86 NH_Note("Invalid 'hash_key' value, '%s', for the strategy named '%s', using default '%s'.", hash_key_val.c_str(),
87 strategy_name.c_str(), hash_key_path.data());
88 }
89 }
90 } catch (std::exception &ex) {
91 NH_Note("Error parsing the strategy named '%s' due to '%s', this strategy will be ignored.", strategy_name.c_str(), ex.what());
92 return false;
93 }
94
95 bool result = NextHopSelectionStrategy::Init(n);
96 if (!result) {
97 return false;
98 }
99
100 // load up the hash rings.
101 for (uint32_t i = 0; i < groups; i++) {
102 std::shared_ptr<ATSConsistentHash> hash_ring = std::make_shared<ATSConsistentHash>();
103 for (uint32_t j = 0; j < host_groups[i].size(); j++) {
104 // ATSConsistentHash needs the raw pointer.
105 HostRecord *p = host_groups[i][j].get();
106 // need to copy the 'hash_string' or 'hostname' cstring to 'name' for insertion into ATSConsistentHash.
107 if (!p->hash_string.empty()) {
108 p->name = const_cast<char *>(p->hash_string.c_str());
109 } else {
110 p->name = const_cast<char *>(p->hostname.c_str());
111 }
112 p->group_index = host_groups[i][j]->group_index;
113 p->host_index = host_groups[i][j]->host_index;
114 hash_ring->insert(p, p->weight, &hash);
115 NH_Debug(NH_DEBUG_TAG, "Loading hash rings - ring: %d, host record: %d, name: %s, hostname: %s, stategy: %s", i, j, p->name,
116 p->hostname.c_str(), strategy_name.c_str());
117 }
118 hash.clear();
119 rings.push_back(std::move(hash_ring));
120 }
121 return true;
122 }
123
124 // returns a hash key calculated from the request and 'hash_key' configuration
125 // parameter.
126 uint64_t
getHashKey(uint64_t sm_id,HttpRequestData * hrdata,ATSHash64 * h)127 NextHopConsistentHash::getHashKey(uint64_t sm_id, HttpRequestData *hrdata, ATSHash64 *h)
128 {
129 URL *url = hrdata->hdr->url_get();
130 URL *ps_url = nullptr;
131 int len = 0;
132 const char *url_string_ref = nullptr;
133
134 // calculate a hash using the selected config.
135 switch (hash_key) {
136 case NH_URL_HASH_KEY:
137 url_string_ref = url->string_get_ref(&len, URLNormalize::LC_SCHEME_HOST);
138 if (url_string_ref && len > 0) {
139 h->update(url_string_ref, len);
140 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] url hash string: %s", sm_id, url_string_ref);
141 }
142 break;
143 // hostname hash
144 case NH_HOSTNAME_HASH_KEY:
145 url_string_ref = url->host_get(&len);
146 if (url_string_ref && len > 0) {
147 h->update(url_string_ref, len);
148 }
149 break;
150 // path + query string
151 case NH_PATH_QUERY_HASH_KEY:
152 url_string_ref = url->path_get(&len);
153 h->update("/", 1);
154 if (url_string_ref && len > 0) {
155 h->update(url_string_ref, len);
156 }
157 url_string_ref = url->query_get(&len);
158 if (url_string_ref && len > 0) {
159 h->update("?", 1);
160 h->update(url_string_ref, len);
161 }
162 break;
163 // path + fragment hash
164 case NH_PATH_FRAGMENT_HASH_KEY:
165 url_string_ref = url->path_get(&len);
166 h->update("/", 1);
167 if (url_string_ref && len > 0) {
168 h->update(url_string_ref, len);
169 }
170 url_string_ref = url->fragment_get(&len);
171 if (url_string_ref && len > 0) {
172 h->update("?", 1);
173 h->update(url_string_ref, len);
174 }
175 break;
176 // use the cache key created by the cache-key plugin.
177 case NH_CACHE_HASH_KEY:
178 ps_url = *(hrdata->cache_info_parent_selection_url);
179 if (ps_url) {
180 url_string_ref = ps_url->string_get_ref(&len);
181 if (url_string_ref && len > 0) {
182 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] using parent selection over-ride string:'%.*s'.", sm_id, len, url_string_ref);
183 h->update(url_string_ref, len);
184 }
185 } else {
186 url_string_ref = url->path_get(&len);
187 h->update("/", 1);
188 if (url_string_ref && len > 0) {
189 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] the parent selection over-ride url is not set, using default path: %s.", sm_id,
190 url_string_ref);
191 h->update(url_string_ref, len);
192 }
193 }
194 break;
195 // use the path as the hash, default.
196 case NH_PATH_HASH_KEY:
197 default:
198 url_string_ref = url->path_get(&len);
199 h->update("/", 1);
200 if (url_string_ref && len > 0) {
201 h->update(url_string_ref, len);
202 }
203 break;
204 }
205
206 h->final();
207 return h->get();
208 }
209
210 void
findNextHop(TSHttpTxn txnp,void * ih,time_t now)211 NextHopConsistentHash::findNextHop(TSHttpTxn txnp, void *ih, time_t now)
212 {
213 HttpSM *sm = reinterpret_cast<HttpSM *>(txnp);
214 ParentResult *result = &sm->t_state.parent_result;
215 HttpRequestData request_info = sm->t_state.request_data;
216 int64_t sm_id = sm->sm_id;
217 int64_t retry_time = sm->t_state.txn_conf->parent_retry_time;
218 time_t _now = now;
219 bool firstcall = false;
220 bool nextHopRetry = false;
221 bool wrapped = false;
222 std::vector<bool> wrap_around(groups, false);
223 uint32_t cur_ring = 0; // there is a hash ring for each host group
224 uint64_t hash_key = 0;
225 uint32_t lookups = 0;
226 ATSHash64Sip24 hash;
227 HostRecord *hostRec = nullptr;
228 std::shared_ptr<HostRecord> pRec = nullptr;
229 HostStatus &pStatus = HostStatus::instance();
230 HostStatus_t host_stat = HostStatus_t::HOST_STATUS_INIT;
231 HostStatRec *hst = nullptr;
232
233 if (result->line_number == -1 && result->result == PARENT_UNDEFINED) {
234 firstcall = true;
235 }
236
237 if (firstcall) {
238 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] firstcall, line_number: %d, result: %s", sm_id, result->line_number,
239 ParentResultStr[result->result]);
240 result->line_number = distance;
241 cur_ring = 0;
242 for (uint32_t i = 0; i < groups; i++) {
243 result->chash_init[i] = false;
244 wrap_around[i] = false;
245 }
246 } else {
247 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] not firstcall, line_number: %d, result: %s", sm_id, result->line_number,
248 ParentResultStr[result->result]);
249 switch (ring_mode) {
250 case NH_ALTERNATE_RING:
251 if (groups > 1) {
252 cur_ring = (result->last_group + 1) % groups;
253 } else {
254 cur_ring = result->last_group;
255 }
256 break;
257 case NH_EXHAUST_RING:
258 default:
259 if (!wrapped) {
260 cur_ring = result->last_group;
261 } else if (groups > 1) {
262 cur_ring = (result->last_group + 1) % groups;
263 }
264 break;
265 }
266 }
267
268 // Do the initial parent look-up.
269 hash_key = getHashKey(sm_id, &request_info, &hash);
270
271 do { // search until we've selected a different parent if !firstcall
272 std::shared_ptr<ATSConsistentHash> r = rings[cur_ring];
273 hostRec = chash_lookup(r, hash_key, &result->chashIter[cur_ring], &wrapped, &hash, &result->chash_init[cur_ring],
274 &result->mapWrapped[cur_ring], sm_id);
275 wrap_around[cur_ring] = wrapped;
276 lookups++;
277 // the 'available' flag is maintained in 'host_groups' and not the hash ring.
278 if (hostRec) {
279 pRec = host_groups[hostRec->group_index][hostRec->host_index];
280 if (firstcall) {
281 hst = (pRec) ? pStatus.getHostStatus(pRec->hostname.c_str()) : nullptr;
282 result->first_choice_status = (hst) ? hst->status : HostStatus_t::HOST_STATUS_UP;
283 break;
284 }
285 } else {
286 pRec = nullptr;
287 }
288 } while (pRec && result->hostname && strcmp(pRec->hostname.c_str(), result->hostname) == 0);
289
290 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] Initial parent lookups: %d", sm_id, lookups);
291
292 // ----------------------------------------------------------------------------------------------------
293 // Validate initial parent look-up and perform additional look-ups if required.
294 // ----------------------------------------------------------------------------------------------------
295
296 hst = (pRec) ? pStatus.getHostStatus(pRec->hostname.c_str()) : nullptr;
297 host_stat = (hst) ? hst->status : HostStatus_t::HOST_STATUS_UP;
298 // if the config ignore_self_detect is set to true and the host is down due to SELF_DETECT reason
299 // ignore the down status and mark it as available
300 if ((pRec && ignore_self_detect) && (hst && hst->status == HOST_STATUS_DOWN)) {
301 if (hst->reasons == Reason::SELF_DETECT) {
302 host_stat = HOST_STATUS_UP;
303 }
304 }
305 if (!pRec || (pRec && !pRec->available) || host_stat == HOST_STATUS_DOWN) {
306 do {
307 // check if an unavailable server is now retryable, use it if it is.
308 if (pRec && !pRec->available && host_stat == HOST_STATUS_UP) {
309 _now == 0 ? _now = time(nullptr) : _now = now;
310 // check if the host is retryable. It's retryable if the retry window has elapsed
311 if ((pRec->failedAt + retry_time) < static_cast<unsigned>(_now)) {
312 nextHopRetry = true;
313 result->last_parent = pRec->host_index;
314 result->last_lookup = pRec->group_index;
315 result->retry = nextHopRetry;
316 result->result = PARENT_SPECIFIED;
317 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] next hop %s is now retryable, marked it available.", sm_id, pRec->hostname.c_str());
318 break;
319 }
320 }
321 switch (ring_mode) {
322 case NH_ALTERNATE_RING:
323 if (pRec && groups > 0) {
324 cur_ring = (pRec->group_index + 1) % groups;
325 } else {
326 cur_ring = 0;
327 }
328 break;
329 case NH_EXHAUST_RING:
330 default:
331 if (wrap_around[cur_ring] && groups > 1) {
332 cur_ring = (cur_ring + 1) % groups;
333 }
334 break;
335 }
336 std::shared_ptr<ATSConsistentHash> r = rings[cur_ring];
337 hostRec = chash_lookup(r, hash_key, &result->chashIter[cur_ring], &wrapped, &hash, &result->chash_init[cur_ring],
338 &result->mapWrapped[cur_ring], sm_id);
339 wrap_around[cur_ring] = wrapped;
340 lookups++;
341 if (hostRec) {
342 pRec = host_groups[hostRec->group_index][hostRec->host_index];
343 hst = (pRec) ? pStatus.getHostStatus(pRec->hostname.c_str()) : nullptr;
344 host_stat = (hst) ? hst->status : HostStatus_t::HOST_STATUS_UP;
345 // if the config ignore_self_detect is set to true and the host is down due to SELF_DETECT reason
346 // ignore the down status and mark it as available
347 if ((pRec && ignore_self_detect) && (hst && hst->status == HOST_STATUS_DOWN)) {
348 if (hst->reasons == Reason::SELF_DETECT) {
349 host_stat = HOST_STATUS_UP;
350 }
351 }
352 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] Selected a new parent: %s, available: %s, wrapped: %s, lookups: %d.", sm_id,
353 pRec->hostname.c_str(), (pRec->available) ? "true" : "false", (wrapped) ? "true" : "false", lookups);
354 // use available host.
355 if (pRec->available && host_stat == HOST_STATUS_UP) {
356 break;
357 }
358 } else {
359 pRec = nullptr;
360 }
361 bool all_wrapped = true;
362 for (uint32_t c = 0; c < groups; c++) {
363 if (wrap_around[c] == false) {
364 all_wrapped = false;
365 }
366 }
367 if (all_wrapped) {
368 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] No available parents.", sm_id);
369 if (pRec) {
370 pRec = nullptr;
371 }
372 break;
373 }
374 } while (!pRec || (pRec && !pRec->available) || host_stat == HOST_STATUS_DOWN);
375 }
376
377 // ----------------------------------------------------------------------------------------------------
378 // Validate and return the final result.
379 // ----------------------------------------------------------------------------------------------------
380
381 if (pRec && host_stat == HOST_STATUS_UP && (pRec->available || result->retry)) {
382 result->result = PARENT_SPECIFIED;
383 result->hostname = pRec->hostname.c_str();
384 result->last_parent = pRec->host_index;
385 result->last_lookup = result->last_group = cur_ring;
386 switch (scheme) {
387 case NH_SCHEME_NONE:
388 case NH_SCHEME_HTTP:
389 result->port = pRec->getPort(scheme);
390 break;
391 case NH_SCHEME_HTTPS:
392 result->port = pRec->getPort(scheme);
393 break;
394 }
395 result->retry = nextHopRetry;
396 ink_assert(result->hostname != nullptr);
397 ink_assert(result->port != 0);
398 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] result->result: %s Chosen parent: %s.%d", sm_id, ParentResultStr[result->result],
399 result->hostname, result->port);
400 } else {
401 if (go_direct == true) {
402 result->result = PARENT_DIRECT;
403 } else {
404 result->result = PARENT_FAIL;
405 }
406 result->hostname = nullptr;
407 result->port = 0;
408 result->retry = false;
409 NH_Debug(NH_DEBUG_TAG, "[%" PRIu64 "] result->result: %s set hostname null port 0 retry false", sm_id,
410 ParentResultStr[result->result]);
411 }
412
413 return;
414 }
415