]> git.ipfire.org Git - thirdparty/squid.git/blob - src/peer_select.cc
Cleaned up the namespace (local functions made static)
[thirdparty/squid.git] / src / peer_select.cc
1
2 /*
3 * $Id: peer_select.cc,v 1.113 2001/02/07 18:56:52 hno Exp $
4 *
5 * DEBUG: section 44 Peer Selection Algorithm
6 * AUTHOR: Duane Wessels
7 *
8 * SQUID Web Proxy Cache http://www.squid-cache.org/
9 * ----------------------------------------------------------
10 *
11 * Squid is the result of efforts by numerous individuals from
12 * the Internet community; see the CONTRIBUTORS file for full
13 * details. Many organizations have provided support for Squid's
14 * development; see the SPONSORS file for full details. Squid is
15 * Copyrighted (C) 2001 by the Regents of the University of
16 * California; see the COPYRIGHT file for full details. Squid
17 * incorporates software developed and/or copyrighted by other
18 * sources; see the CREDITS file for full details.
19 *
20 * This program is free software; you can redistribute it and/or modify
21 * it under the terms of the GNU General Public License as published by
22 * the Free Software Foundation; either version 2 of the License, or
23 * (at your option) any later version.
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * You should have received a copy of the GNU General Public License
31 * along with this program; if not, write to the Free Software
32 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
33 *
34 */
35
36 #include "squid.h"
37
38 const char *hier_strings[] =
39 {
40 "NONE",
41 "DIRECT",
42 "SIBLING_HIT",
43 "PARENT_HIT",
44 "DEFAULT_PARENT",
45 "SINGLE_PARENT",
46 "FIRST_UP_PARENT",
47 "FIRST_PARENT_MISS",
48 "CLOSEST_PARENT_MISS",
49 "CLOSEST_PARENT",
50 "CLOSEST_DIRECT",
51 "NO_DIRECT_FAIL",
52 "SOURCE_FASTEST",
53 "ROUNDROBIN_PARENT",
54 #if USE_CACHE_DIGESTS
55 "CD_PARENT_HIT",
56 "CD_SIBLING_HIT",
57 #endif
58 #if USE_CARP
59 "CARP",
60 #endif
61 "ANY_PARENT",
62 "INVALID CODE"
63 };
64
65 static struct {
66 int timeouts;
67 } PeerStats;
68
69 static char *DirectStr[] =
70 {
71 "DIRECT_UNKNOWN",
72 "DIRECT_NO",
73 "DIRECT_MAYBE",
74 "DIRECT_YES"
75 };
76
77 static void peerSelectFoo(ps_state *);
78 static void peerPingTimeout(void *data);
79 static void peerSelectCallback(ps_state * psstate);
80 static IRCB peerHandlePingReply;
81 static void peerSelectStateFree(ps_state * psstate);
82 static void peerIcpParentMiss(peer *, icp_common_t *, ps_state *);
83 #if USE_HTCP
84 static void peerHtcpParentMiss(peer *, htcpReplyData *, ps_state *);
85 static void peerHandleHtcpReply(peer *, peer_t, htcpReplyData *, void *);
86 #endif
87 static int peerCheckNetdbDirect(ps_state * psstate);
88 static void peerGetSomeNeighbor(ps_state *);
89 static void peerGetSomeNeighborReplies(ps_state *);
90 static void peerGetSomeDirect(ps_state *);
91 static void peerGetSomeParent(ps_state *);
92 static void peerGetAllParents(ps_state *);
93 static void peerAddFwdServer(FwdServer **, peer *, hier_code);
94
95 static void
96 peerSelectStateFree(ps_state * psstate)
97 {
98 if (psstate->acl_checklist) {
99 debug(44, 1) ("calling aclChecklistFree() from peerSelectStateFree\n");
100 aclChecklistFree(psstate->acl_checklist);
101 }
102 requestUnlink(psstate->request);
103 psstate->request = NULL;
104 if (psstate->entry) {
105 assert(psstate->entry->ping_status != PING_WAITING);
106 storeUnlockObject(psstate->entry);
107 psstate->entry = NULL;
108 }
109 cbdataFree(psstate);
110 }
111
112 static int
113 peerSelectIcpPing(request_t * request, int direct, StoreEntry * entry)
114 {
115 int n;
116 assert(entry);
117 assert(entry->ping_status == PING_NONE);
118 assert(direct != DIRECT_YES);
119 debug(44, 3) ("peerSelectIcpPing: %s\n", storeUrl(entry));
120 if (!request->flags.hierarchical && direct != DIRECT_NO)
121 return 0;
122 if (EBIT_TEST(entry->flags, KEY_PRIVATE) && !neighbors_do_private_keys)
123 if (direct != DIRECT_NO)
124 return 0;
125 n = neighborsCount(request);
126 debug(44, 3) ("peerSelectIcpPing: counted %d neighbors\n", n);
127 return n;
128 }
129
130
131 void
132 peerSelect(request_t * request,
133 StoreEntry * entry,
134 PSC * callback,
135 void *callback_data)
136 {
137 ps_state *psstate;
138 if (entry)
139 debug(44, 3) ("peerSelect: %s\n", storeUrl(entry));
140 else
141 debug(44, 3) ("peerSelect: %s\n", RequestMethodStr[request->method]);
142 psstate = CBDATA_ALLOC(ps_state, NULL);
143 psstate->request = requestLink(request);
144 psstate->entry = entry;
145 psstate->callback = callback;
146 psstate->callback_data = callback_data;
147 psstate->direct = DIRECT_UNKNOWN;
148 #if USE_CACHE_DIGESTS
149 request->hier.peer_select_start = current_time;
150 #endif
151 if (psstate->entry)
152 storeLockObject(psstate->entry);
153 cbdataLock(callback_data);
154 peerSelectFoo(psstate);
155 }
156
157 static void
158 peerCheckNeverDirectDone(int answer, void *data)
159 {
160 ps_state *psstate = data;
161 psstate->acl_checklist = NULL;
162 debug(44, 3) ("peerCheckNeverDirectDone: %d\n", answer);
163 psstate->never_direct = answer ? 1 : -1;
164 peerSelectFoo(psstate);
165 }
166
167 static void
168 peerCheckAlwaysDirectDone(int answer, void *data)
169 {
170 ps_state *psstate = data;
171 psstate->acl_checklist = NULL;
172 debug(44, 3) ("peerCheckAlwaysDirectDone: %d\n", answer);
173 psstate->always_direct = answer ? 1 : -1;
174 peerSelectFoo(psstate);
175 }
176
177 static void
178 peerSelectCallback(ps_state * psstate)
179 {
180 StoreEntry *entry = psstate->entry;
181 FwdServer *fs = psstate->servers;
182 void *data = psstate->callback_data;
183 if (entry) {
184 debug(44, 3) ("peerSelectCallback: %s\n", storeUrl(entry));
185 if (entry->ping_status == PING_WAITING)
186 eventDelete(peerPingTimeout, psstate);
187 entry->ping_status = PING_DONE;
188 }
189 if (fs == NULL) {
190 debug(44, 1) ("Failed to select source for '%s'\n", storeUrl(entry));
191 debug(44, 1) (" always_direct = %d\n", psstate->always_direct);
192 debug(44, 1) (" never_direct = %d\n", psstate->never_direct);
193 debug(44, 1) (" timedout = %d\n", psstate->ping.timedout);
194 }
195 psstate->ping.stop = current_time;
196 psstate->request->hier.ping = psstate->ping;
197 if (cbdataValid(data)) {
198 psstate->servers = NULL;
199 psstate->callback(fs, data);
200 }
201 cbdataUnlock(data);
202 peerSelectStateFree(psstate);
203 }
204
205 static int
206 peerCheckNetdbDirect(ps_state * psstate)
207 {
208 peer *p;
209 int myrtt;
210 int myhops;
211 if (psstate->direct == DIRECT_NO)
212 return 0;
213 myrtt = netdbHostRtt(psstate->request->host);
214 debug(44, 3) ("peerCheckNetdbDirect: MY RTT = %d msec\n", myrtt);
215 debug(44, 3) ("peerCheckNetdbDirect: minimum_direct_rtt = %d msec\n",
216 Config.minDirectRtt);
217 if (myrtt && myrtt <= Config.minDirectRtt)
218 return 1;
219 myhops = netdbHostHops(psstate->request->host);
220 debug(44, 3) ("peerCheckNetdbDirect: MY hops = %d\n", myhops);
221 debug(44, 3) ("peerCheckNetdbDirect: minimum_direct_hops = %d\n",
222 Config.minDirectHops);
223 if (myhops && myhops <= Config.minDirectHops)
224 return 1;
225 p = whichPeer(&psstate->closest_parent_miss);
226 if (p == NULL)
227 return 0;
228 debug(44, 3) ("peerCheckNetdbDirect: closest_parent_miss RTT = %d msec\n",
229 psstate->ping.p_rtt);
230 if (myrtt && myrtt <= psstate->ping.p_rtt)
231 return 1;
232 return 0;
233 }
234
235 static void
236 peerSelectFoo(ps_state * ps)
237 {
238 StoreEntry *entry = ps->entry;
239 request_t *request = ps->request;
240 debug(44, 3) ("peerSelectFoo: '%s %s'\n",
241 RequestMethodStr[request->method],
242 request->host);
243 if (ps->direct == DIRECT_UNKNOWN) {
244 if (ps->always_direct == 0 && Config.accessList.AlwaysDirect) {
245 ps->acl_checklist = aclChecklistCreate(
246 Config.accessList.AlwaysDirect,
247 request,
248 NULL); /* ident */
249 aclNBCheck(ps->acl_checklist,
250 peerCheckAlwaysDirectDone,
251 ps);
252 return;
253 } else if (ps->always_direct > 0) {
254 ps->direct = DIRECT_YES;
255 } else if (ps->never_direct == 0 && Config.accessList.NeverDirect) {
256 ps->acl_checklist = aclChecklistCreate(
257 Config.accessList.NeverDirect,
258 request,
259 NULL); /* ident */
260 aclNBCheck(ps->acl_checklist,
261 peerCheckNeverDirectDone,
262 ps);
263 return;
264 } else if (ps->never_direct > 0) {
265 ps->direct = DIRECT_NO;
266 } else if (request->flags.loopdetect) {
267 ps->direct = DIRECT_YES;
268 } else if (peerCheckNetdbDirect(ps)) {
269 ps->direct = DIRECT_YES;
270 } else {
271 ps->direct = DIRECT_MAYBE;
272 }
273 debug(44, 3) ("peerSelectFoo: direct = %s\n",
274 DirectStr[ps->direct]);
275 }
276 if (entry == NULL) {
277 (void) 0;
278 } else if (entry->ping_status == PING_NONE) {
279 peerGetSomeNeighbor(ps);
280 if (entry->ping_status == PING_WAITING)
281 return;
282 } else if (entry->ping_status == PING_WAITING) {
283 peerGetSomeNeighborReplies(ps);
284 entry->ping_status = PING_DONE;
285 }
286 switch (ps->direct) {
287 case DIRECT_YES:
288 peerGetSomeDirect(ps);
289 break;
290 case DIRECT_NO:
291 peerGetSomeParent(ps);
292 peerGetAllParents(ps);
293 break;
294 default:
295 if (Config.onoff.prefer_direct)
296 peerGetSomeDirect(ps);
297 if (request->flags.hierarchical || !Config.onoff.nonhierarchical_direct)
298 peerGetSomeParent(ps);
299 if (!Config.onoff.prefer_direct)
300 peerGetSomeDirect(ps);
301 break;
302 }
303 peerSelectCallback(ps);
304 }
305
306 /*
307 * peerGetSomeNeighbor
308 *
309 * Selects a neighbor (parent or sibling) based on one of the
310 * following methods:
311 * Cache Digests
312 * CARP
313 * Netdb RTT estimates
314 * ICP/HTCP queries
315 */
316 static void
317 peerGetSomeNeighbor(ps_state * ps)
318 {
319 StoreEntry *entry = ps->entry;
320 request_t *request = ps->request;
321 peer *p;
322 hier_code code = HIER_NONE;
323 assert(entry->ping_status == PING_NONE);
324 if (ps->direct == DIRECT_YES) {
325 entry->ping_status = PING_DONE;
326 return;
327 }
328 #if USE_CACHE_DIGESTS
329 if ((p = neighborsDigestSelect(request, entry))) {
330 if (neighborType(p, request) == PEER_PARENT)
331 code = CD_PARENT_HIT;
332 else
333 code = CD_SIBLING_HIT;
334 } else
335 #endif
336 #if USE_CARP
337 if ((p = carpSelectParent(request))) {
338 code = CARP;
339 } else
340 #endif
341 if ((p = netdbClosestParent(request))) {
342 code = CLOSEST_PARENT;
343 } else if (peerSelectIcpPing(request, ps->direct, entry)) {
344 debug(44, 3) ("peerSelect: Doing ICP pings\n");
345 ps->ping.start = current_time;
346 ps->ping.n_sent = neighborsUdpPing(request,
347 entry,
348 peerHandlePingReply,
349 ps,
350 &ps->ping.n_replies_expected,
351 &ps->ping.timeout);
352 if (ps->ping.n_sent == 0)
353 debug(44, 0) ("WARNING: neighborsUdpPing returned 0\n");
354 debug(44, 3) ("peerSelect: %d ICP replies expected, RTT %d msec\n",
355 ps->ping.n_replies_expected, ps->ping.timeout);
356 if (ps->ping.n_replies_expected > 0) {
357 entry->ping_status = PING_WAITING;
358 eventAdd("peerPingTimeout",
359 peerPingTimeout,
360 ps,
361 0.001 * ps->ping.timeout,
362 0);
363 return;
364 }
365 }
366 if (code != HIER_NONE) {
367 assert(p);
368 debug(44, 3) ("peerSelect: %s/%s\n", hier_strings[code], p->host);
369 peerAddFwdServer(&ps->servers, p, code);
370 }
371 entry->ping_status = PING_DONE;
372 }
373
374 /*
375 * peerGetSomeNeighborReplies
376 *
377 * Selects a neighbor (parent or sibling) based on ICP/HTCP replies.
378 */
379 static void
380 peerGetSomeNeighborReplies(ps_state * ps)
381 {
382 request_t *request = ps->request;
383 peer *p = NULL;
384 hier_code code = HIER_NONE;
385 assert(ps->entry->ping_status == PING_WAITING);
386 assert(ps->direct != DIRECT_YES);
387 if (peerCheckNetdbDirect(ps)) {
388 code = CLOSEST_DIRECT;
389 debug(44, 3) ("peerSelect: %s/%s\n", hier_strings[code], request->host);
390 peerAddFwdServer(&ps->servers, NULL, code);
391 return;
392 }
393 if ((p = ps->hit)) {
394 code = ps->hit_type == PEER_PARENT ? PARENT_HIT : SIBLING_HIT;
395 } else
396 #if ALLOW_SOURCE_PING
397 if ((p = ps->secho)) {
398 code = SOURCE_FASTEST;
399 } else
400 #endif
401 if (ps->closest_parent_miss.sin_addr.s_addr != any_addr.s_addr) {
402 p = whichPeer(&ps->closest_parent_miss);
403 code = CLOSEST_PARENT_MISS;
404 } else if (ps->first_parent_miss.sin_addr.s_addr != any_addr.s_addr) {
405 p = whichPeer(&ps->first_parent_miss);
406 code = FIRST_PARENT_MISS;
407 }
408 if (p && code != HIER_NONE) {
409 debug(44, 3) ("peerSelect: %s/%s\n", hier_strings[code], p->host);
410 peerAddFwdServer(&ps->servers, p, code);
411 }
412 }
413
414
415 /*
416 * peerGetSomeDirect
417 *
418 * Simply adds a 'direct' entry to the FwdServers list if this
419 * request can be forwarded directly to the origin server
420 */
421 static void
422 peerGetSomeDirect(ps_state * ps)
423 {
424 if (ps->direct == DIRECT_NO)
425 return;
426 if (ps->request->protocol == PROTO_WAIS)
427 /* Its not really DIRECT, now is it? */
428 peerAddFwdServer(&ps->servers, Config.Wais.peer, DIRECT);
429 else
430 peerAddFwdServer(&ps->servers, NULL, DIRECT);
431 }
432
433 static void
434 peerGetSomeParent(ps_state * ps)
435 {
436 peer *p;
437 request_t *request = ps->request;
438 hier_code code = HIER_NONE;
439 debug(44, 3) ("peerGetSomeParent: %s %s\n",
440 RequestMethodStr[request->method],
441 request->host);
442 if (ps->direct == DIRECT_YES)
443 return;
444 if ((p = getDefaultParent(request))) {
445 code = DEFAULT_PARENT;
446 } else if ((p = getRoundRobinParent(request))) {
447 code = ROUNDROBIN_PARENT;
448 } else if ((p = getFirstUpParent(request))) {
449 code = FIRSTUP_PARENT;
450 } else if ((p = getAnyParent(request))) {
451 code = ANY_OLD_PARENT;
452 }
453 if (code != HIER_NONE) {
454 debug(44, 3) ("peerSelect: %s/%s\n", hier_strings[code], p->host);
455 peerAddFwdServer(&ps->servers, p, code);
456 }
457 }
458
459 /* Adds alive parents. Used as a last resort for never_direct.
460 */
461 static void
462 peerGetAllParents(ps_state * ps)
463 {
464 peer *p;
465 request_t *request = ps->request;
466 /* Add all alive parents */
467 for (p = Config.peers; p; p = p->next) {
468 /* XXX: neighbors.c lacks a public interface for enumerating
469 * parents to a request so we have to dig some here..
470 */
471 if (neighborType(p, request) != PEER_PARENT)
472 continue;
473 if (!peerHTTPOkay(p, request))
474 continue;
475 debug(15, 3) ("peerGetAllParents: adding alive parent %s\n", p->host);
476 peerAddFwdServer(&ps->servers, p, ANY_OLD_PARENT);
477 }
478 /* XXX: should add dead parents here, but it is currently
479 * not possible to find out which parents are dead or which
480 * simply are not configured to handle the request.
481 */
482 /* Add default parent as a last resort */
483 if ((p = getDefaultParent(request))) {
484 peerAddFwdServer(&ps->servers, p, DEFAULT_PARENT);
485 }
486 }
487
488 static void
489 peerPingTimeout(void *data)
490 {
491 ps_state *psstate = data;
492 StoreEntry *entry = psstate->entry;
493 if (entry)
494 debug(44, 3) ("peerPingTimeout: '%s'\n", storeUrl(entry));
495 if (!cbdataValid(psstate->callback_data)) {
496 /* request aborted */
497 entry->ping_status = PING_DONE;
498 cbdataUnlock(psstate->callback_data);
499 peerSelectStateFree(psstate);
500 return;
501 }
502 PeerStats.timeouts++;
503 psstate->ping.timedout = 1;
504 peerSelectFoo(psstate);
505 }
506
507 void
508 peerSelectInit(void)
509 {
510 memset(&PeerStats, '\0', sizeof(PeerStats));
511 assert(sizeof(hier_strings) == (HIER_MAX + 1) * sizeof(char *));
512 }
513
514 static void
515 peerIcpParentMiss(peer * p, icp_common_t * header, ps_state * ps)
516 {
517 int rtt;
518 int hops;
519 if (Config.onoff.query_icmp) {
520 if (header->flags & ICP_FLAG_SRC_RTT) {
521 rtt = header->pad & 0xFFFF;
522 hops = (header->pad >> 16) & 0xFFFF;
523 if (rtt > 0 && rtt < 0xFFFF)
524 netdbUpdatePeer(ps->request, p, rtt, hops);
525 if (rtt && (ps->ping.p_rtt == 0 || rtt < ps->ping.p_rtt)) {
526 ps->closest_parent_miss = p->in_addr;
527 ps->ping.p_rtt = rtt;
528 }
529 }
530 }
531 /* if closest-only is set, then don't allow FIRST_PARENT_MISS */
532 if (p->options.closest_only)
533 return;
534 /* set FIRST_MISS if there is no CLOSEST parent */
535 if (ps->closest_parent_miss.sin_addr.s_addr != any_addr.s_addr)
536 return;
537 rtt = tvSubMsec(ps->ping.start, current_time) / p->weight;
538 if (ps->ping.w_rtt == 0 || rtt < ps->ping.w_rtt) {
539 ps->first_parent_miss = p->in_addr;
540 ps->ping.w_rtt = rtt;
541 }
542 }
543
544 static void
545 peerHandleIcpReply(peer * p, peer_t type, icp_common_t * header, void *data)
546 {
547 ps_state *psstate = data;
548 icp_opcode op = header->opcode;
549 debug(44, 3) ("peerHandleIcpReply: %s %s\n",
550 icp_opcode_str[op],
551 storeUrl(psstate->entry));
552 #if USE_CACHE_DIGESTS && 0
553 /* do cd lookup to count false misses */
554 if (p && request)
555 peerNoteDigestLookup(request, p,
556 peerDigestLookup(p, request, psstate->entry));
557 #endif
558 psstate->ping.n_recv++;
559 if (op == ICP_MISS || op == ICP_DECHO) {
560 if (type == PEER_PARENT)
561 peerIcpParentMiss(p, header, psstate);
562 } else if (op == ICP_HIT) {
563 psstate->hit = p;
564 psstate->hit_type = type;
565 peerSelectFoo(psstate);
566 return;
567 }
568 #if ALLOW_SOURCE_PING
569 else if (op == ICP_SECHO) {
570 psstate->secho = p;
571 peerSelectFoo(psstate);
572 return;
573 }
574 #endif
575 if (psstate->ping.n_recv < psstate->ping.n_replies_expected)
576 return;
577 peerSelectFoo(psstate);
578 }
579
580 #if USE_HTCP
581 static void
582 peerHandleHtcpReply(peer * p, peer_t type, htcpReplyData * htcp, void *data)
583 {
584 ps_state *psstate = data;
585 debug(44, 3) ("peerHandleIcpReply: %s %s\n",
586 htcp->hit ? "HIT" : "MISS",
587 storeUrl(psstate->entry));
588 psstate->ping.n_recv++;
589 if (htcp->hit) {
590 psstate->hit = p;
591 psstate->hit_type = type;
592 peerSelectFoo(psstate);
593 return;
594 }
595 if (type == PEER_PARENT)
596 peerHtcpParentMiss(p, htcp, psstate);
597 if (psstate->ping.n_recv < psstate->ping.n_replies_expected)
598 return;
599 peerSelectFoo(psstate);
600 }
601
602 static void
603 peerHtcpParentMiss(peer * p, htcpReplyData * htcp, ps_state * ps)
604 {
605 int rtt;
606 int hops;
607 if (Config.onoff.query_icmp) {
608 if (htcp->cto.rtt > 0) {
609 rtt = (int) htcp->cto.rtt * 1000;
610 hops = (int) htcp->cto.hops * 1000;
611 netdbUpdatePeer(ps->request, p, rtt, hops);
612 if (rtt && (ps->ping.p_rtt == 0 || rtt < ps->ping.p_rtt)) {
613 ps->closest_parent_miss = p->in_addr;
614 ps->ping.p_rtt = rtt;
615 }
616 }
617 }
618 /* if closest-only is set, then don't allow FIRST_PARENT_MISS */
619 if (p->options.closest_only)
620 return;
621 /* set FIRST_MISS if there is no CLOSEST parent */
622 if (ps->closest_parent_miss.sin_addr.s_addr != any_addr.s_addr)
623 return;
624 rtt = tvSubMsec(ps->ping.start, current_time) / p->weight;
625 if (ps->ping.w_rtt == 0 || rtt < ps->ping.w_rtt) {
626 ps->first_parent_miss = p->in_addr;
627 ps->ping.w_rtt = rtt;
628 }
629 }
630 #endif
631
632 static void
633 peerHandlePingReply(peer * p, peer_t type, protocol_t proto, void *pingdata, void *data)
634 {
635 if (proto == PROTO_ICP)
636 peerHandleIcpReply(p, type, pingdata, data);
637 #if USE_HTCP
638 else if (proto == PROTO_HTCP)
639 peerHandleHtcpReply(p, type, pingdata, data);
640 #endif
641 else
642 debug(44, 1) ("peerHandlePingReply: unknown protocol_t %d\n", (int) proto);
643 }
644
645 static void
646 peerAddFwdServer(FwdServer ** FS, peer * p, hier_code code)
647 {
648 FwdServer *fs = memAllocate(MEM_FWD_SERVER);
649 debug(44, 5) ("peerAddFwdServer: adding %s %s\n",
650 p ? p->host : "DIRECT",
651 hier_strings[code]);
652 fs->peer = p;
653 fs->code = code;
654 cbdataLock(fs->peer);
655 while (*FS)
656 FS = &(*FS)->next;
657 *FS = fs;
658 }