1 /** @file
2 
3   Implementation of Parent Proxy routing
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 #include <atomic>
24 #include "HostStatus.h"
25 #include "ParentConsistentHash.h"
26 
ParentConsistentHash(ParentRecord * parent_record)27 ParentConsistentHash::ParentConsistentHash(ParentRecord *parent_record)
28 {
29   int i;
30 
31   ink_assert(parent_record->num_parents > 0);
32   parents[PRIMARY]   = parent_record->parents;
33   parents[SECONDARY] = parent_record->secondary_parents;
34   ignore_query       = parent_record->ignore_query;
35   secondary_mode     = parent_record->secondary_mode;
36   ink_zero(foundParents);
37 
38   chash[PRIMARY] = new ATSConsistentHash();
39 
40   for (i = 0; i < parent_record->num_parents; i++) {
41     chash[PRIMARY]->insert(&(parent_record->parents[i]), parent_record->parents[i].weight, (ATSHash64 *)&hash[PRIMARY]);
42   }
43 
44   if (parent_record->num_secondary_parents > 0) {
45     Debug("parent_select", "ParentConsistentHash(): initializing the secondary parents hash.");
46     chash[SECONDARY] = new ATSConsistentHash();
47 
48     for (i = 0; i < parent_record->num_secondary_parents; i++) {
49       chash[SECONDARY]->insert(&(parent_record->secondary_parents[i]), parent_record->secondary_parents[i].weight,
50                                (ATSHash64 *)&hash[SECONDARY]);
51     }
52   } else {
53     chash[SECONDARY] = nullptr;
54   }
55   Debug("parent_select", "Using a consistent hash parent selection strategy.");
56 }
57 
~ParentConsistentHash()58 ParentConsistentHash::~ParentConsistentHash()
59 {
60   Debug("parent_select", "~ParentConsistentHash(): releasing hashes");
61   delete chash[PRIMARY];
62   delete chash[SECONDARY];
63 }
64 
65 uint64_t
getPathHash(HttpRequestData * hrdata,ATSHash64 * h)66 ParentConsistentHash::getPathHash(HttpRequestData *hrdata, ATSHash64 *h)
67 {
68   const char *url_string_ref = nullptr;
69   int len;
70   URL *url = hrdata->hdr->url_get();
71 
72   // Use over-ride URL from HttpTransact::State's cache_info.parent_selection_url, if present.
73   URL *ps_url = nullptr;
74   if (hrdata->cache_info_parent_selection_url) {
75     ps_url = *(hrdata->cache_info_parent_selection_url);
76     if (ps_url) {
77       url_string_ref = ps_url->string_get_ref(&len);
78       if (url_string_ref && len > 0) {
79         // Print the over-ride URL
80         Debug("parent_select", "Using Over-Ride String='%.*s'.", len, url_string_ref);
81         h->update(url_string_ref, len);
82         h->final();
83         return h->get();
84       }
85     }
86   }
87 
88   // Always hash on '/' because paths returned by ATS are always stripped of it
89   h->update("/", 1);
90 
91   url_string_ref = url->path_get(&len);
92   if (url_string_ref) {
93     h->update(url_string_ref, len);
94   }
95 
96   if (!ignore_query) {
97     url_string_ref = url->query_get(&len);
98     if (url_string_ref) {
99       h->update("?", 1);
100       h->update(url_string_ref, len);
101     }
102   }
103 
104   h->final();
105 
106   return h->get();
107 }
108 
109 // Helper function to abstract calling ATSConsistentHash lookup_by_hashval() vs lookup().
110 static pRecord *
chash_lookup(ATSConsistentHash * fhash,uint64_t path_hash,ATSConsistentHashIter * chashIter,bool * wrap_around,ATSHash64Sip24 * hash,bool * chash_init,bool * mapWrapped)111 chash_lookup(ATSConsistentHash *fhash, uint64_t path_hash, ATSConsistentHashIter *chashIter, bool *wrap_around,
112              ATSHash64Sip24 *hash, bool *chash_init, bool *mapWrapped)
113 {
114   pRecord *prtmp;
115 
116   if (*chash_init == false) {
117     prtmp       = (pRecord *)fhash->lookup_by_hashval(path_hash, chashIter, wrap_around);
118     *chash_init = true;
119   } else {
120     prtmp = (pRecord *)fhash->lookup(nullptr, chashIter, wrap_around, hash);
121   }
122   // Do not set wrap_around to true until we try all the parents at least once.
123   bool wrapped = *wrap_around;
124   *wrap_around = (*mapWrapped && *wrap_around) ? true : false;
125   if (!*mapWrapped && wrapped) {
126     *mapWrapped = true;
127   }
128   return prtmp;
129 }
130 
131 void
selectParent(bool first_call,ParentResult * result,RequestData * rdata,unsigned int fail_threshold,unsigned int retry_time)132 ParentConsistentHash::selectParent(bool first_call, ParentResult *result, RequestData *rdata, unsigned int fail_threshold,
133                                    unsigned int retry_time)
134 {
135   ATSHash64Sip24 hash;
136   ATSConsistentHash *fhash;
137   HttpRequestData *request_info = static_cast<HttpRequestData *>(rdata);
138   bool firstCall                = first_call;
139   bool parentRetry              = false;
140   bool wrap_around[2]           = {false, false};
141   int lookups                   = 0;
142   uint64_t path_hash            = 0;
143   uint32_t last_lookup;
144   pRecord *prtmp = nullptr, *pRec = nullptr;
145   HostStatus &pStatus    = HostStatus::instance();
146   HostStatus_t host_stat = HostStatus_t::HOST_STATUS_INIT;
147 
148   Debug("parent_select", "ParentConsistentHash::%s(): Using a consistent hash parent selection strategy.", __func__);
149   ink_assert(numParents(result) > 0 || result->rec->go_direct == true);
150 
151   // Should only get into this state if we are supposed to go direct.
152   if (parents[PRIMARY] == nullptr && parents[SECONDARY] == nullptr) {
153     if (result->rec->go_direct == true && result->rec->parent_is_proxy == true) {
154       result->result = PARENT_DIRECT;
155     } else {
156       result->result = PARENT_FAIL;
157     }
158     result->hostname = nullptr;
159     result->port     = 0;
160     return;
161   }
162 
163   // ----------------------------------------------------------------------------------------------------
164   // Initial parent look-up for either findParent() (firstCall) or nextParent() (!firstCall)
165   // ----------------------------------------------------------------------------------------------------
166 
167   // firstCall means called from findParent() and always use PRIMARY parent list.
168   if (firstCall) {
169     last_lookup = PRIMARY;
170   } else {
171     // !firstCall means called from nextParent() and must determine which parent list to use.
172     switch (secondary_mode) {
173     case 2:
174       last_lookup = PRIMARY;
175       break;
176     case 3:
177       if (result->first_choice_status == HOST_STATUS_DOWN && chash[SECONDARY] != nullptr) {
178         last_lookup = SECONDARY;
179       } else {
180         last_lookup = PRIMARY;
181       }
182       break;
183     case 1:
184     default:
185       if (chash[SECONDARY] != nullptr) {
186         last_lookup = SECONDARY;
187       } else {
188         last_lookup = PRIMARY;
189       }
190     }
191   }
192 
193   // Do the initial parent look-up.
194   path_hash = getPathHash(request_info, (ATSHash64 *)&hash);
195   fhash     = chash[last_lookup];
196   do { // search until we've selected a different parent if !firstCall
197     prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], &hash,
198                          &result->chash_init[last_lookup], &result->mapWrapped[last_lookup]);
199     lookups++;
200     if (prtmp) {
201       pRec = (parents[last_lookup] + prtmp->idx);
202     } else {
203       pRec = nullptr;
204     }
205     if (firstCall) {
206       break;
207     }
208   } while (pRec && !firstCall && last_lookup == PRIMARY && strcmp(pRec->hostname, result->hostname) == 0);
209 
210   Debug("parent_select", "Initial parent lookups: %d", lookups);
211 
212   // ----------------------------------------------------------------------------------------------------
213   // Validate initial parent look-up and perform additional look-ups if required.
214   // ----------------------------------------------------------------------------------------------------
215 
216   // didn't find a parent or the parent is marked unavailable or the parent is marked down
217   HostStatRec *hst = (pRec) ? pStatus.getHostStatus(pRec->hostname) : nullptr;
218   host_stat        = (hst) ? hst->status : HostStatus_t::HOST_STATUS_UP;
219   if (firstCall) {
220     result->first_choice_status = host_stat;
221   }
222 
223   // if the config ignore_self_detect is set to true and the host is down due to SELF_DETECT reason
224   // ignore the down status and mark it as available
225   if ((pRec && result->rec->ignore_self_detect) && (hst && hst->status == HOST_STATUS_DOWN)) {
226     if (hst->reasons == Reason::SELF_DETECT) {
227       host_stat = HOST_STATUS_UP;
228     }
229   }
230   if (!pRec || (pRec && !pRec->available.load()) || host_stat == HOST_STATUS_DOWN) {
231     do {
232       // check if the host is retryable.  It's retryable if the retry window has elapsed
233       // and the global host status is HOST_STATUS_UP
234       if (pRec && !pRec->available.load() && host_stat == HOST_STATUS_UP) {
235         Debug("parent_select", "Parent.failedAt = %u, retry = %u, xact_start = %u", static_cast<unsigned>(pRec->failedAt.load()),
236               static_cast<unsigned>(retry_time), static_cast<unsigned>(request_info->xact_start));
237         if ((pRec->failedAt.load() + retry_time) < request_info->xact_start) {
238           if (pRec->retriers.fetch_add(1, std::memory_order_relaxed) < max_retriers) {
239             parentRetry = true;
240             // make sure that the proper state is recorded in the result structure
241             result->last_parent = pRec->idx;
242             result->last_lookup = last_lookup;
243             result->retry       = parentRetry;
244             result->result      = PARENT_SPECIFIED;
245             Debug("parent_select", "Down parent %s is now retryable, retriers = %d, max_retriers = %d", pRec->hostname,
246                   pRec->retriers.load(), max_retriers);
247             break;
248           } else {
249             pRec->retriers--;
250           }
251         }
252       }
253       Debug("parent_select", "wrap_around[PRIMARY]: %d, wrap_around[SECONDARY]: %d", wrap_around[PRIMARY], wrap_around[SECONDARY]);
254       if (!wrap_around[PRIMARY] || (chash[SECONDARY] != nullptr && !wrap_around[SECONDARY])) {
255         Debug("parent_select", "Selected parent %s is not available, looking up another parent.", pRec ? pRec->hostname : "[NULL]");
256         switch (secondary_mode) {
257         case 2:
258           if (!wrap_around[PRIMARY]) {
259             last_lookup = PRIMARY;
260           } else if (chash[SECONDARY] != nullptr && !wrap_around[SECONDARY]) {
261             last_lookup = SECONDARY;
262           }
263           break;
264         case 3:
265           if (result->first_choice_status == HOST_STATUS_DOWN) {
266             if (chash[SECONDARY] != nullptr && !wrap_around[SECONDARY]) {
267               last_lookup = SECONDARY;
268             } else if (!wrap_around[PRIMARY]) {
269               last_lookup = PRIMARY;
270             }
271           } else {
272             if (!wrap_around[PRIMARY]) {
273               last_lookup = PRIMARY;
274             } else if (chash[SECONDARY] != nullptr && !wrap_around[SECONDARY]) {
275               last_lookup = SECONDARY;
276             }
277           }
278           break;
279         case 1:
280         default:
281           if (chash[SECONDARY] != nullptr && !wrap_around[SECONDARY]) {
282             last_lookup = SECONDARY;
283           } else if (!wrap_around[PRIMARY]) {
284             last_lookup = PRIMARY;
285           }
286         }
287         fhash = chash[last_lookup];
288         prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], &hash,
289                              &result->chash_init[last_lookup], &result->mapWrapped[last_lookup]);
290         lookups++;
291         if (prtmp) {
292           pRec = (parents[last_lookup] + prtmp->idx);
293           Debug("parent_select", "Selected a new parent: %s.", pRec->hostname);
294         } else {
295           pRec = nullptr;
296         }
297       }
298       if (wrap_around[PRIMARY] && chash[SECONDARY] == nullptr) {
299         Debug("parent_select", "No available parents.");
300         break;
301       }
302       if (wrap_around[PRIMARY] && chash[SECONDARY] != nullptr && wrap_around[SECONDARY]) {
303         Debug("parent_select", "No available parents.");
304         break;
305       }
306       hst       = (pRec) ? pStatus.getHostStatus(pRec->hostname) : nullptr;
307       host_stat = (hst) ? hst->status : HostStatus_t::HOST_STATUS_UP;
308       // if the config ignore_self_detect is set to true and the host is down due to SELF_DETECT reason
309       // ignore the down status and mark it as available
310       if ((pRec && result->rec->ignore_self_detect) && (hst && hst->status == HOST_STATUS_DOWN)) {
311         if (hst->reasons == Reason::SELF_DETECT) {
312           host_stat = HOST_STATUS_UP;
313         }
314       }
315     } while (!pRec || !pRec->available.load() || host_stat == HOST_STATUS_DOWN);
316   }
317 
318   Debug("parent_select", "Additional parent lookups: %d", lookups);
319 
320   // ----------------------------------------------------------------------------------------------------
321   // Validate and return the final result.
322   // ----------------------------------------------------------------------------------------------------
323 
324   // if the config ignore_self_detect is set to true and the host is down due to SELF_DETECT reason
325   // ignore the down status and mark it as available
326   if ((pRec && result->rec->ignore_self_detect) && (hst && hst->status == HOST_STATUS_DOWN)) {
327     if (hst->reasons == Reason::SELF_DETECT) {
328       host_stat = HOST_STATUS_UP;
329     }
330   }
331   if (pRec && host_stat == HOST_STATUS_UP && (pRec->available.load() || result->retry)) {
332     result->result      = PARENT_SPECIFIED;
333     result->hostname    = pRec->hostname;
334     result->port        = pRec->port;
335     result->last_parent = pRec->idx;
336     result->last_lookup = last_lookup;
337     result->retry       = parentRetry;
338     ink_assert(result->hostname != nullptr);
339     ink_assert(result->port != 0);
340     Debug("parent_select", "Chosen parent: %s.%d", result->hostname, result->port);
341   } else {
342     if (result->rec->go_direct == true && result->rec->parent_is_proxy == true) {
343       result->result = PARENT_DIRECT;
344     } else {
345       result->result = PARENT_FAIL;
346     }
347     result->hostname = nullptr;
348     result->port     = 0;
349     result->retry    = false;
350   }
351 
352   return;
353 }
354 
355 uint32_t
numParents(ParentResult * result) const356 ParentConsistentHash::numParents(ParentResult *result) const
357 {
358   uint32_t n = 0;
359 
360   switch (result->last_lookup) {
361   case PRIMARY:
362     n = result->rec->num_parents;
363     break;
364   case SECONDARY:
365     n = result->rec->num_secondary_parents;
366     break;
367   }
368 
369   return n;
370 }
371 
372 void
markParentUp(ParentResult * result)373 ParentConsistentHash::markParentUp(ParentResult *result)
374 {
375   pRecord *pRec;
376 
377   //  Make sure that we are being called back with with a
378   //   result structure with a parent that is being retried
379   ink_release_assert(result->retry == true);
380   ink_assert(result->result == PARENT_SPECIFIED);
381   if (result->result != PARENT_SPECIFIED) {
382     return;
383   }
384   // If we were set through the API we currently have not failover
385   //   so just return fail
386   if (result->is_api_result()) {
387     ink_assert(0);
388     return;
389   }
390 
391   ink_assert((result->last_parent) < numParents(result));
392   pRec            = parents[result->last_lookup] + result->last_parent;
393   pRec->available = true;
394   Debug("parent_select", "%s:%s(): marked %s:%d available.", __FILE__, __func__, pRec->hostname, pRec->port);
395 
396   pRec->failedAt = static_cast<time_t>(0);
397   int old_count  = pRec->failCount.exchange(0, std::memory_order_relaxed);
398   pRec->retriers = 0;
399 
400   if (old_count > 0) {
401     Note("http parent proxy %s:%d restored", pRec->hostname, pRec->port);
402   }
403 }
404