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