]> git.ipfire.org Git - thirdparty/squid.git/blame - src/peer_digest.cc
Supply ALE with HttpReply before checking http_reply_access (#398)
[thirdparty/squid.git] / src / peer_digest.cc
CommitLineData
9b7de833 1/*
f6e9a3ee 2 * Copyright (C) 1996-2019 The Squid Software Foundation and contributors
e25c139f 3 *
bbc27441
AJ
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9b7de833 7 */
8
bbc27441
AJ
9/* DEBUG: section 72 Peer Digest Routines */
10
582c2af2 11#include "squid.h"
6cfa8966 12#if USE_CACHE_DIGESTS
b814e8d4 13#include "CacheDigest.h"
a011edee 14#include "CachePeer.h"
a553a5a3 15#include "event.h"
eb13c21e 16#include "FwdState.h"
af69c635 17#include "globals.h"
528b2c61 18#include "HttpReply.h"
582c2af2 19#include "HttpRequest.h"
308e60be 20#include "internal.h"
528b2c61 21#include "MemObject.h"
b6149797 22#include "mime_header.h"
602d9612 23#include "neighbors.h"
aa839030 24#include "PeerDigest.h"
985c86bc 25#include "SquidTime.h"
582c2af2 26#include "Store.h"
fb548aaf 27#include "store_key_md5.h"
598465f1 28#include "StoreClient.h"
4e540555 29#include "tools.h"
ed6e9fb9 30#include "util.h"
598465f1 31
9b7de833 32/* local types */
33
34/* local prototypes */
e13ee7ad 35static time_t peerDigestIncDelay(const PeerDigest * pd);
36static time_t peerDigestNewDelay(const StoreEntry * e);
37static void peerDigestSetCheck(PeerDigest * pd, time_t delay);
38static EVH peerDigestCheck;
39static void peerDigestRequest(PeerDigest * pd);
add2192d 40static STCB peerDigestHandleReply;
3dfaa3d2 41static int peerDigestFetchReply(void *, char *, ssize_t);
1f140227 42int peerDigestSwapInHeaders(void *, char *, ssize_t);
43int peerDigestSwapInCBlock(void *, char *, ssize_t);
44int peerDigestSwapInMask(void *, char *, ssize_t);
4b4cd312 45static int peerDigestFetchedEnough(DigestFetchState * fetch, char *buf, ssize_t size, const char *step_name);
551e60a9 46static void peerDigestFetchStop(DigestFetchState * fetch, char *buf, const char *reason);
e13ee7ad 47static void peerDigestFetchAbort(DigestFetchState * fetch, char *buf, const char *reason);
48static void peerDigestReqFinish(DigestFetchState * fetch, char *buf, int, int, int, const char *reason, int err);
49static void peerDigestPDFinish(DigestFetchState * fetch, int pcb_valid, int err);
50static void peerDigestFetchFinish(DigestFetchState * fetch, int err);
51static void peerDigestFetchSetStats(DigestFetchState * fetch);
52static int peerDigestSetCBlock(PeerDigest * pd, const char *buf);
53static int peerDigestUseful(const PeerDigest * pd);
395b813e 54
9b7de833 55/* local constants */
aa839030 56Version const CacheDigestVer = { 5, 3 };
9d486b43 57
9b7de833 58#define StoreDigestCBlockSize sizeof(StoreDigestCBlock)
59
e13ee7ad 60/* min interval for requesting digests from a given peer */
f53969cc 61static const time_t PeerDigestReqMinGap = 5 * 60; /* seconds */
e13ee7ad 62/* min interval for requesting digests (cumulative request stream) */
f53969cc 63static const time_t GlobDigestReqMinGap = 1 * 60; /* seconds */
bd890734 64
65/* local vars */
9d486b43 66
f53969cc 67static time_t pd_last_req_time = 0; /* last call to Check */
9b7de833 68
b56b37cf 69PeerDigest::PeerDigest(CachePeer * p)
9b7de833 70{
b56b37cf 71 assert(p);
e13ee7ad 72
4e454ade 73 /*
74 * DPW 2007-04-12
75 * Lock on to the peer here. The corresponding cbdataReferenceDone()
76 * is in peerDigestDestroy().
77 */
b56b37cf 78 peer = cbdataReference(p);
e13ee7ad 79 /* if peer disappears, we will know it's name */
b56b37cf 80 host = p->host;
e13ee7ad 81
b56b37cf 82 times.initialized = squid_curtime;
9b7de833 83}
84
0353e724 85CBDATA_CLASS_INIT(PeerDigest);
86
a44986dc
AJ
87CBDATA_CLASS_INIT(DigestFetchState);
88
022099cb 89DigestFetchState::DigestFetchState(PeerDigest *aPd, HttpRequest *req) :
a44986dc
AJ
90 pd(cbdataReference(aPd)),
91 entry(NULL),
92 old_entry(NULL),
93 sc(NULL),
94 old_sc(NULL),
95 request(req),
96 offset(0),
97 mask_offset(0),
98 start_time(squid_curtime),
99 resp_time(0),
100 expires(0),
101 bufofs(0),
102 state(DIGEST_READ_REPLY)
103{
104 HTTPMSGLOCK(request);
105
106 sent.msg = 0;
107 sent.bytes = 0;
108
109 recv.msg = 0;
110 recv.bytes = 0;
111
112 *buf = 0;
113}
114
115DigestFetchState::~DigestFetchState()
116{
117 /* unlock everything */
118 storeUnregister(sc, entry, this);
119
120 entry->unlock("DigestFetchState destructed");
121 entry = NULL;
122
123 HTTPMSGUNLOCK(request);
124
125 assert(pd == NULL);
126}
127
e13ee7ad 128/* allocate new peer digest, call Init, and lock everything */
b56b37cf 129void
a3c6762c 130peerDigestCreate(CachePeer * p)
e13ee7ad 131{
8a6218c6 132 assert(p);
e13ee7ad 133
b56b37cf
AJ
134 PeerDigest *pd = new PeerDigest(p);
135
136 // TODO: make CachePeer member a CbcPointer
137 p->digest = cbdataReference(pd);
e13ee7ad 138
b56b37cf
AJ
139 // lock a reference to pd again to prevent the PeerDigest
140 // disappearing during peerDigestDestroy() when
141 // cbdataReferenceValidDone is called.
142 // TODO test if it can be moved into peerDigestDestroy() or
143 // if things can break earlier (eg CachePeer death).
144 (void)cbdataReference(pd);
e13ee7ad 145}
146
147/* call Clean and free/unlock everything */
2d72d4fd 148static void
8a6218c6 149peerDigestDestroy(PeerDigest * pd)
e13ee7ad 150{
8abf232c 151 void *p;
e13ee7ad 152 assert(pd);
8abf232c 153 void * peerTmp = pd->peer;
e13ee7ad 154
4e454ade 155 /*
156 * DPW 2007-04-12
b56b37cf
AJ
157 * We locked the peer in PeerDigest constructor, this is
158 * where we unlock it.
4e454ade 159 */
b56b37cf
AJ
160 if (cbdataReferenceValidDone(peerTmp, &p)) {
161 // we locked the p->digest in peerDigestCreate()
162 // this is where we unlock that
163 cbdataReferenceDone(static_cast<CachePeer *>(p)->digest);
164 }
62e76326 165
00d77d6b 166 delete pd;
e13ee7ad 167}
168
b56b37cf
AJ
169PeerDigest::~PeerDigest()
170{
171 delete cd;
172 // req_result pointer is not owned by us
173}
174
e13ee7ad 175/* called by peer to indicate that somebody actually needs this digest */
176void
8a6218c6 177peerDigestNeeded(PeerDigest * pd)
e13ee7ad 178{
179 assert(pd);
180 assert(!pd->flags.needed);
181 assert(!pd->cd);
182
be4d35dc 183 pd->flags.needed = true;
e13ee7ad 184 pd->times.needed = squid_curtime;
f53969cc 185 peerDigestSetCheck(pd, 0); /* check asap */
e13ee7ad 186}
187
188/* currently we do not have a reason to disable without destroying */
8a6218c6 189#if FUTURE_CODE
395b813e 190/* disables peer for good */
191static void
8a6218c6 192peerDigestDisable(PeerDigest * pd)
395b813e 193{
bf8fe701 194 debugs(72, 2, "peerDigestDisable: peer " << pd->host.buf() << " disabled for good");
e13ee7ad 195 pd->times.disabled = squid_curtime;
f53969cc 196 pd->times.next_check = -1; /* never */
e13ee7ad 197 pd->flags.usable = 0;
198
a901d0b4
AJ
199 delete pd->cd
200 pd->cd = nullptr;
62e76326 201
e13ee7ad 202 /* we do not destroy the pd itself to preserve its "history" and stats */
395b813e 203}
62e76326 204
e13ee7ad 205#endif
395b813e 206
e13ee7ad 207/* increment retry delay [after an unsuccessful attempt] */
395b813e 208static time_t
8a6218c6 209peerDigestIncDelay(const PeerDigest * pd)
395b813e 210{
e13ee7ad 211 assert(pd);
212 return pd->times.retry_delay > 0 ?
f53969cc
SM
213 2 * pd->times.retry_delay : /* exponential backoff */
214 PeerDigestReqMinGap; /* minimal delay */
395b813e 215}
216
62e76326 217/* artificially increases Expires: setting to avoid race conditions
e13ee7ad 218 * returns the delay till that [increased] expiration time */
00485c29 219static time_t
e13ee7ad 220peerDigestNewDelay(const StoreEntry * e)
00485c29 221{
e13ee7ad 222 assert(e);
62e76326 223
00485c29 224 if (e->expires > 0)
62e76326 225 return e->expires + PeerDigestReqMinGap - squid_curtime;
226
e13ee7ad 227 return PeerDigestReqMinGap;
00485c29 228}
229
e13ee7ad 230/* registers next digest verification */
395b813e 231static void
e13ee7ad 232peerDigestSetCheck(PeerDigest * pd, time_t delay)
395b813e 233{
e13ee7ad 234 eventAdd("peerDigestCheck", peerDigestCheck, pd, (double) delay, 1);
235 pd->times.next_check = squid_curtime + delay;
bb790702 236 debugs(72, 3, "peerDigestSetCheck: will check peer " << pd->host << " in " << delay << " secs");
e13ee7ad 237}
238
5385c86a 239/*
240 * called when peer is about to disappear or have already disappeared
241 */
e13ee7ad 242void
8a6218c6 243peerDigestNotePeerGone(PeerDigest * pd)
244{
e13ee7ad 245 if (pd->flags.requested) {
bb790702 246 debugs(72, 2, "peerDigest: peer " << pd->host << " gone, will destroy after fetch.");
62e76326 247 /* do nothing now, the fetching chain will notice and take action */
395b813e 248 } else {
bb790702 249 debugs(72, 2, "peerDigest: peer " << pd->host << " is gone, destroying now.");
62e76326 250 peerDigestDestroy(pd);
395b813e 251 }
252}
253
e13ee7ad 254/* callback for eventAdd() (with peer digest locked)
26ac0430 255 * request new digest if our copy is too old or if we lack one;
e13ee7ad 256 * schedule next check otherwise */
9b7de833 257static void
e13ee7ad 258peerDigestCheck(void *data)
9b7de833 259{
e6ccf245 260 PeerDigest *pd = (PeerDigest *)data;
e13ee7ad 261 time_t req_time;
262
e13ee7ad 263 assert(!pd->flags.requested);
5d9bb360 264
f53969cc 265 pd->times.next_check = 0; /* unknown */
e13ee7ad 266
fa80a8ef 267 if (!cbdataReferenceValid(pd->peer)) {
62e76326 268 peerDigestNotePeerGone(pd);
269 return;
9b7de833 270 }
62e76326 271
cc192b50 272 debugs(72, 3, "peerDigestCheck: peer " << pd->peer->host << ":" << pd->peer->http_port);
26ac0430
AJ
273 debugs(72, 3, "peerDigestCheck: time: " << squid_curtime <<
274 ", last received: " << (long int) pd->times.received << " (" <<
bf8fe701 275 std::showpos << (int) (squid_curtime - pd->times.received) << ")");
e13ee7ad 276
277 /* decide when we should send the request:
278 * request now unless too close to other requests */
279 req_time = squid_curtime;
280
281 /* per-peer limit */
62e76326 282
e13ee7ad 283 if (req_time - pd->times.received < PeerDigestReqMinGap) {
b4197865 284 debugs(72, 2, "peerDigestCheck: " << pd->host <<
bf8fe701 285 ", avoiding close peer requests (" <<
286 (int) (req_time - pd->times.received) << " < " <<
287 (int) PeerDigestReqMinGap << " secs).");
288
62e76326 289 req_time = pd->times.received + PeerDigestReqMinGap;
bd890734 290 }
62e76326 291
e13ee7ad 292 /* global limit */
293 if (req_time - pd_last_req_time < GlobDigestReqMinGap) {
b4197865 294 debugs(72, 2, "peerDigestCheck: " << pd->host <<
bf8fe701 295 ", avoiding close requests (" <<
296 (int) (req_time - pd_last_req_time) << " < " <<
297 (int) GlobDigestReqMinGap << " secs).");
298
62e76326 299 req_time = pd_last_req_time + GlobDigestReqMinGap;
9b7de833 300 }
62e76326 301
e13ee7ad 302 if (req_time <= squid_curtime)
f53969cc 303 peerDigestRequest(pd); /* will set pd->flags.requested */
e13ee7ad 304 else
62e76326 305 peerDigestSetCheck(pd, req_time - squid_curtime);
9b7de833 306}
307
e13ee7ad 308/* ask store for a digest */
9b7de833 309static void
e13ee7ad 310peerDigestRequest(PeerDigest * pd)
9b7de833 311{
a3c6762c 312 CachePeer *p = pd->peer;
9b7de833 313 StoreEntry *e, *old_e;
7e928efc 314 char *url = NULL;
190154cf 315 HttpRequest *req;
528b2c61 316 StoreIOBuffer tempBuffer;
e13ee7ad 317
318 pd->req_result = NULL;
be4d35dc 319 pd->flags.requested = true;
e13ee7ad 320
9b7de833 321 /* compute future request components */
62e76326 322
7e3ce7b9 323 if (p->digest_url)
62e76326 324 url = xstrdup(p->digest_url);
7e3ce7b9 325 else
c002f0e7 326 url = xstrdup(internalRemoteUri(p->secure.encryptTransport, p->host, p->http_port, "/squid-internal-periodic/", SBuf(StoreDigestFileName)));
4310f8b0 327 debugs(72, 2, url);
7e3ce7b9 328
5ceaee75
CT
329 const MasterXaction::Pointer mx = new MasterXaction(XactionInitiator::initCacheDigest);
330 req = HttpRequest::FromUrl(url, mx);
62e76326 331
e13ee7ad 332 assert(req);
62e76326 333
9b7de833 334 /* add custom headers */
2246b732 335 assert(!req->header.len);
62e76326 336
789217a2 337 req->header.putStr(Http::HdrType::ACCEPT, StoreDigestMimeStr);
62e76326 338
789217a2 339 req->header.putStr(Http::HdrType::ACCEPT, "text/html");
62e76326 340
89000349
AJ
341 if (p->login &&
342 p->login[0] != '*' &&
b552ea1f
A
343 strcmp(p->login, "PASS") != 0 &&
344 strcmp(p->login, "PASSTHRU") != 0 &&
98ace3e4 345 strncmp(p->login, "NEGOTIATE",9) != 0 &&
89000349 346 strcmp(p->login, "PROXYPASS") != 0) {
c486d50a 347 req->url.userInfo(SBuf(p->login)); // XXX: performance regression make peer login SBuf as well.
89000349 348 }
9b7de833 349 /* create fetch state structure */
022099cb 350 DigestFetchState *fetch = new DigestFetchState(pd, req);
e13ee7ad 351
352 /* update timestamps */
e13ee7ad 353 pd->times.requested = squid_curtime;
354 pd_last_req_time = squid_curtime;
e857372a 355 req->flags.cachable = true;
62e76326 356
4310f8b0 357 /* the rest is based on clientReplyContext::processExpired() */
e857372a 358 req->flags.refresh = true;
62e76326 359
4310f8b0 360 old_e = fetch->old_entry = storeGetPublicByRequest(req);
62e76326 361
d2a6dcba 362 // XXX: Missing a hittingRequiresCollapsing() && startCollapsingOn() check.
9b7de833 363 if (old_e) {
4310f8b0 364 debugs(72, 5, "found old " << *old_e);
34266cde 365
acc5dc4c 366 old_e->lock("peerDigestRequest");
dba7b575 367 old_e->ensureMemObject(url, url, req->method);
34266cde 368
62e76326 369 fetch->old_sc = storeClientListAdd(old_e, fetch);
9b7de833 370 }
62e76326 371
9b7de833 372 e = fetch->entry = storeCreateEntry(url, url, req->flags, req->method);
4310f8b0 373 debugs(72, 5, "created " << *e);
e13ee7ad 374 assert(EBIT_TEST(e->flags, KEY_PRIVATE));
06d2839d 375 fetch->sc = storeClientListAdd(e, fetch);
9b7de833 376 /* set lastmod to trigger IMS request if possible */
62e76326 377
9b7de833 378 if (old_e)
438b41ba 379 e->lastModified(old_e->lastModified());
e13ee7ad 380
9b7de833 381 /* push towards peer cache */
e83cc785 382 FwdState::fwdStart(Comm::ConnectionPointer(), e, req);
62e76326 383
598465f1 384 tempBuffer.offset = 0;
62e76326 385
f95db407 386 tempBuffer.length = SM_PAGE_SIZE;
62e76326 387
598465f1 388 tempBuffer.data = fetch->buf;
62e76326 389
598465f1 390 storeClientCopy(fetch->sc, e, tempBuffer,
62e76326 391 peerDigestHandleReply, fetch);
7e928efc
AJ
392
393 safe_free(url);
9b7de833 394}
395
add2192d 396/* Handle the data copying .. */
397
398/*
399 * This routine handles the copy data and then redirects the
400 * copy to a bunch of subfunctions depending upon the copy state.
401 * It also tracks the buffer offset and "seen", since I'm actually
402 * not interested in rewriting everything to suit my little idea.
403 */
9b7de833 404static void
2324cda2 405peerDigestHandleReply(void *data, StoreIOBuffer receivedData)
add2192d 406{
e6ccf245 407 DigestFetchState *fetch = (DigestFetchState *)data;
add2192d 408 int retsize = -1;
409 digest_read_state_t prevstate;
410 int newsize;
411
2324cda2 412 assert(fetch->pd && receivedData.data);
413 /* The existing code assumes that the received pointer is
598465f1 414 * where we asked the data to be put
415 */
2324cda2 416 assert(fetch->buf + fetch->bufofs == receivedData.data);
add2192d 417
418 /* Update the buffer size */
2324cda2 419 fetch->bufofs += receivedData.length;
add2192d 420
421 assert(fetch->bufofs <= SM_PAGE_SIZE);
422
423 /* If we've fetched enough, return */
62e76326 424
add2192d 425 if (peerDigestFetchedEnough(fetch, fetch->buf, fetch->bufofs, "peerDigestHandleReply"))
62e76326 426 return;
add2192d 427
428 /* Call the right function based on the state */
429 /* (Those functions will update the state if needed) */
fa80a8ef 430
e2848e94 431 /* Give us a temporary reference. Some of the calls we make may
432 * try to destroy the fetch structure, and we like to know if they
433 * do
bac6d4bd 434 */
a44986dc 435 CbcPointer<DigestFetchState> tmpLock = fetch;
add2192d 436
437 /* Repeat this loop until we're out of data OR the state changes */
438 /* (So keep going if the state has changed and we still have data */
439 do {
62e76326 440 prevstate = fetch->state;
441
442 switch (fetch->state) {
443
444 case DIGEST_READ_REPLY:
445 retsize = peerDigestFetchReply(fetch, fetch->buf, fetch->bufofs);
446 break;
447
448 case DIGEST_READ_HEADERS:
449 retsize = peerDigestSwapInHeaders(fetch, fetch->buf, fetch->bufofs);
450 break;
451
452 case DIGEST_READ_CBLOCK:
453 retsize = peerDigestSwapInCBlock(fetch, fetch->buf, fetch->bufofs);
454 break;
455
456 case DIGEST_READ_MASK:
457 retsize = peerDigestSwapInMask(fetch, fetch->buf, fetch->bufofs);
458 break;
459
460 case DIGEST_READ_NONE:
461 break;
462
463 case DIGEST_READ_DONE:
a44986dc 464 return;
62e76326 465 break;
466
467 default:
468 fatal("Bad digest transfer mode!\n");
469 }
470
471 if (retsize < 0)
a44986dc 472 return;
62e76326 473
474 /*
475 * The returned size indicates how much of the buffer was read -
476 * so move the remainder of the buffer to the beginning
477 * and update the bufofs / bufsize
478 */
479 newsize = fetch->bufofs - retsize;
480
41d00cd3 481 memmove(fetch->buf, fetch->buf + retsize, fetch->bufofs - newsize);
62e76326 482
483 fetch->bufofs = newsize;
add2192d 484
e2848e94 485 } while (cbdataReferenceValid(fetch) && prevstate != fetch->state && fetch->bufofs > 0);
add2192d 486
487 /* Update the copy offset */
2324cda2 488 fetch->offset += receivedData.length;
add2192d 489
490 /* Schedule another copy */
fa80a8ef 491 if (cbdataReferenceValid(fetch)) {
62e76326 492 StoreIOBuffer tempBuffer;
493 tempBuffer.offset = fetch->offset;
494 tempBuffer.length = SM_PAGE_SIZE - fetch->bufofs;
495 tempBuffer.data = fetch->buf + fetch->bufofs;
496 storeClientCopy(fetch->sc, fetch->entry, tempBuffer,
497 peerDigestHandleReply, fetch);
add2192d 498 }
add2192d 499}
500
add2192d 501/* wait for full http headers to be received then parse them */
502/*
503 * This routine handles parsing the reply line.
504 * If the reply line indicates an OK, the same data is thrown
505 * to SwapInHeaders(). If the reply line is a NOT_MODIFIED,
506 * we simply stop parsing.
507 */
508static int
9b7de833 509peerDigestFetchReply(void *data, char *buf, ssize_t size)
510{
e6ccf245 511 DigestFetchState *fetch = (DigestFetchState *)data;
e13ee7ad 512 PeerDigest *pd = fetch->pd;
9bc73deb 513 size_t hdr_size;
e13ee7ad 514 assert(pd && buf);
9b7de833 515 assert(!fetch->offset);
e13ee7ad 516
add2192d 517 assert(fetch->state == DIGEST_READ_REPLY);
62e76326 518
9b7de833 519 if (peerDigestFetchedEnough(fetch, buf, size, "peerDigestFetchReply"))
62e76326 520 return -1;
e13ee7ad 521
9bc73deb 522 if ((hdr_size = headersEnd(buf, size))) {
62e76326 523 HttpReply const *reply = fetch->entry->getReply();
524 assert(reply);
9b769c67
AJ
525 assert(reply->sline.status() != Http::scNone);
526 const Http::StatusCode status = reply->sline.status();
b4197865 527 debugs(72, 3, "peerDigestFetchReply: " << pd->host << " status: " << status <<
26ac0430 528 ", expires: " << (long int) reply->expires << " (" << std::showpos <<
bf8fe701 529 (int) (reply->expires - squid_curtime) << ")");
62e76326 530
531 /* this "if" is based on clientHandleIMSReply() */
532
955394ce 533 if (status == Http::scNotModified) {
62e76326 534 /* our old entry is fine */
535 assert(fetch->old_entry);
536
e2cc8c07 537 if (!fetch->old_entry->mem_obj->request)
b248c2a3 538 fetch->old_entry->mem_obj->request = fetch->entry->mem_obj->request;
62e76326 539
540 assert(fetch->old_entry->mem_obj->request);
541
abf396ec 542 Store::Root().updateOnNotModified(fetch->old_entry, *fetch->entry);
62e76326 543
544 /* get rid of 304 reply */
545 storeUnregister(fetch->sc, fetch->entry, fetch);
546
acc5dc4c 547 fetch->entry->unlock("peerDigestFetchReply 304");
62e76326 548
549 fetch->entry = fetch->old_entry;
550
551 fetch->old_entry = NULL;
552
553 /* preserve request -- we need its size to update counters */
554 /* requestUnlink(r); */
555 /* fetch->entry->mem_obj->request = NULL; */
955394ce 556 } else if (status == Http::scOkay) {
62e76326 557 /* get rid of old entry if any */
558
559 if (fetch->old_entry) {
bf8fe701 560 debugs(72, 3, "peerDigestFetchReply: got new digest, releasing old one");
62e76326 561 storeUnregister(fetch->old_sc, fetch->old_entry, fetch);
d88e3c49 562 fetch->old_entry->releaseRequest();
acc5dc4c 563 fetch->old_entry->unlock("peerDigestFetchReply 200");
62e76326 564 fetch->old_entry = NULL;
565 }
566 } else {
567 /* some kind of a bug */
9b769c67 568 peerDigestFetchAbort(fetch, buf, reply->sline.reason());
f53969cc 569 return -1; /* XXX -1 will abort stuff in ReadReply! */
62e76326 570 }
571
572 /* must have a ready-to-use store entry if we got here */
573 /* can we stay with the old in-memory digest? */
955394ce 574 if (status == Http::scNotModified && fetch->pd->cd) {
62e76326 575 peerDigestFetchStop(fetch, buf, "Not modified");
576 fetch->state = DIGEST_READ_DONE;
577 } else {
578 fetch->state = DIGEST_READ_HEADERS;
579 }
9b7de833 580 } else {
62e76326 581 /* need more data, do we have space? */
582
583 if (size >= SM_PAGE_SIZE)
584 peerDigestFetchAbort(fetch, buf, "reply header too big");
9b7de833 585 }
add2192d 586
587 /* We don't want to actually ack that we've handled anything,
588 * otherwise SwapInHeaders() won't get the reply line .. */
589 return 0;
9b7de833 590}
591
592/* fetch headers from disk, pass on to SwapInCBlock */
1f140227 593int
9b7de833 594peerDigestSwapInHeaders(void *data, char *buf, ssize_t size)
595{
e6ccf245 596 DigestFetchState *fetch = (DigestFetchState *)data;
9b7de833 597 size_t hdr_size;
e13ee7ad 598
add2192d 599 assert(fetch->state == DIGEST_READ_HEADERS);
62e76326 600
9b7de833 601 if (peerDigestFetchedEnough(fetch, buf, size, "peerDigestSwapInHeaders"))
62e76326 602 return -1;
e13ee7ad 603
9d486b43 604 assert(!fetch->offset);
62e76326 605
9b7de833 606 if ((hdr_size = headersEnd(buf, size))) {
62e76326 607 assert(fetch->entry->getReply());
9b769c67 608 assert(fetch->entry->getReply()->sline.status() != Http::scNone);
62e76326 609
9b769c67 610 if (fetch->entry->getReply()->sline.status() != Http::scOkay) {
e0236918 611 debugs(72, DBG_IMPORTANT, "peerDigestSwapInHeaders: " << fetch->pd->host <<
9b769c67 612 " status " << fetch->entry->getReply()->sline.status() <<
bf8fe701 613 " got cached!");
614
62e76326 615 peerDigestFetchAbort(fetch, buf, "internal status error");
616 return -1;
617 }
618
619 fetch->state = DIGEST_READ_CBLOCK;
f53969cc 620 return hdr_size; /* Say how much data we read */
cfd861ab 621 }
62e76326 622
cfd861ab
AJ
623 /* need more data, do we have space? */
624 if (size >= SM_PAGE_SIZE) {
625 peerDigestFetchAbort(fetch, buf, "stored header too big");
626 return -1;
9b7de833 627 }
62e76326 628
cfd861ab 629 return 0; /* We need to read more to parse .. */
9b7de833 630}
631
1f140227 632int
9b7de833 633peerDigestSwapInCBlock(void *data, char *buf, ssize_t size)
634{
e6ccf245 635 DigestFetchState *fetch = (DigestFetchState *)data;
e13ee7ad 636
add2192d 637 assert(fetch->state == DIGEST_READ_CBLOCK);
62e76326 638
9b7de833 639 if (peerDigestFetchedEnough(fetch, buf, size, "peerDigestSwapInCBlock"))
62e76326 640 return -1;
e13ee7ad 641
e6ccf245 642 if (size >= (ssize_t)StoreDigestCBlockSize) {
62e76326 643 PeerDigest *pd = fetch->pd;
62e76326 644
e4a67a80 645 assert(pd && fetch->entry->getReply());
62e76326 646
647 if (peerDigestSetCBlock(pd, buf)) {
648 /* XXX: soon we will have variable header size */
649 /* switch to CD buffer and fetch digest guts */
650 buf = NULL;
651 assert(pd->cd->mask);
652 fetch->state = DIGEST_READ_MASK;
653 return StoreDigestCBlockSize;
654 } else {
655 peerDigestFetchAbort(fetch, buf, "invalid digest cblock");
656 return -1;
657 }
cfd861ab 658 }
62e76326 659
cfd861ab
AJ
660 /* need more data, do we have space? */
661 if (size >= SM_PAGE_SIZE) {
662 peerDigestFetchAbort(fetch, buf, "digest cblock too big");
663 return -1;
9b7de833 664 }
62e76326 665
cfd861ab 666 return 0; /* We need more data */
9b7de833 667}
668
1f140227 669int
9b7de833 670peerDigestSwapInMask(void *data, char *buf, ssize_t size)
671{
e6ccf245 672 DigestFetchState *fetch = (DigestFetchState *)data;
e13ee7ad 673 PeerDigest *pd;
674
e13ee7ad 675 pd = fetch->pd;
676 assert(pd->cd && pd->cd->mask);
9d486b43 677
add2192d 678 /*
679 * NOTENOTENOTENOTENOTE: buf doesn't point to pd->cd->mask anymore!
680 * we need to do the copy ourselves!
681 */
41d00cd3 682 memcpy(pd->cd->mask + fetch->mask_offset, buf, size);
add2192d 683
684 /* NOTE! buf points to the middle of pd->cd->mask! */
62e76326 685
add2192d 686 if (peerDigestFetchedEnough(fetch, NULL, size, "peerDigestSwapInMask"))
62e76326 687 return -1;
add2192d 688
da407def 689 fetch->mask_offset += size;
62e76326 690
57d55dfa 691 if (fetch->mask_offset >= pd->cd->mask_size) {
e4049756 692 debugs(72, 2, "peerDigestSwapInMask: Done! Got " <<
693 fetch->mask_offset << ", expected " << pd->cd->mask_size);
57d55dfa 694 assert(fetch->mask_offset == pd->cd->mask_size);
62e76326 695 assert(peerDigestFetchedEnough(fetch, NULL, 0, "peerDigestSwapInMask"));
f53969cc 696 return -1; /* XXX! */
9b7de833 697 }
62e76326 698
cfd861ab
AJ
699 /* We always read everything, so return size */
700 return size;
9b7de833 701}
702
703static int
4b4cd312 704peerDigestFetchedEnough(DigestFetchState * fetch, char *buf, ssize_t size, const char *step_name)
9b7de833 705{
0e3b8c9f
AJ
706 static const SBuf hostUnknown("<unknown>"); // peer host (if any)
707 SBuf host = hostUnknown;
708
e13ee7ad 709 PeerDigest *pd = NULL;
f53969cc
SM
710 const char *reason = NULL; /* reason for completion */
711 const char *no_bug = NULL; /* successful completion if set */
e2848e94 712 const int pdcb_valid = cbdataReferenceValid(fetch->pd);
713 const int pcb_valid = cbdataReferenceValid(fetch->pd->peer);
e13ee7ad 714
715 /* test possible exiting conditions (the same for most steps!)
716 * cases marked with '?!' should not happen */
717
718 if (!reason) {
62e76326 719 if (!(pd = fetch->pd))
720 reason = "peer digest disappeared?!";
721
f53969cc 722#if DONT /* WHY NOT? /HNO */
62e76326 723
724 else if (!cbdataReferenceValid(pd))
725 reason = "invalidated peer digest?!";
726
5385c86a 727#endif
62e76326 728
729 else
0e3b8c9f 730 host = pd->host;
e13ee7ad 731 }
62e76326 732
e4049756 733 debugs(72, 6, step_name << ": peer " << host << ", offset: " <<
734 fetch->offset << " size: " << size << ".");
e13ee7ad 735
736 /* continue checking (with pd and host known and valid) */
62e76326 737
e13ee7ad 738 if (!reason) {
62e76326 739 if (!cbdataReferenceValid(pd->peer))
740 reason = "peer disappeared";
741 else if (size < 0)
742 reason = "swap failure";
743 else if (!fetch->entry)
744 reason = "swap aborted?!";
745 else if (EBIT_TEST(fetch->entry->flags, ENTRY_ABORTED))
746 reason = "swap aborted";
e13ee7ad 747 }
62e76326 748
e13ee7ad 749 /* continue checking (maybe-successful eof case) */
750 if (!reason && !size) {
62e76326 751 if (!pd->cd)
752 reason = "null digest?!";
831e953c 753 else if (fetch->mask_offset != pd->cd->mask_size)
62e76326 754 reason = "premature end of digest?!";
755 else if (!peerDigestUseful(pd))
756 reason = "useless digest";
757 else
758 reason = no_bug = "success";
e13ee7ad 759 }
62e76326 760
e13ee7ad 761 /* finish if we have a reason */
9b7de833 762 if (reason) {
62e76326 763 const int level = strstr(reason, "?!") ? 1 : 3;
bf8fe701 764 debugs(72, level, "" << step_name << ": peer " << host << ", exiting after '" << reason << "'");
62e76326 765 peerDigestReqFinish(fetch, buf,
766 1, pdcb_valid, pcb_valid, reason, !no_bug);
e13ee7ad 767 } else {
62e76326 768 /* paranoid check */
769 assert(pdcb_valid && pcb_valid);
e13ee7ad 770 }
62e76326 771
e13ee7ad 772 return reason != NULL;
773}
774
551e60a9 775/* call this when all callback data is valid and fetch must be stopped but
776 * no error has occurred (e.g. we received 304 reply and reuse old digest) */
777static void
778peerDigestFetchStop(DigestFetchState * fetch, char *buf, const char *reason)
779{
780 assert(reason);
b4197865 781 debugs(72, 2, "peerDigestFetchStop: peer " << fetch->pd->host << ", reason: " << reason);
551e60a9 782 peerDigestReqFinish(fetch, buf, 1, 1, 1, reason, 0);
783}
784
e13ee7ad 785/* call this when all callback data is valid but something bad happened */
786static void
787peerDigestFetchAbort(DigestFetchState * fetch, char *buf, const char *reason)
788{
551e60a9 789 assert(reason);
b4197865 790 debugs(72, 2, "peerDigestFetchAbort: peer " << fetch->pd->host << ", reason: " << reason);
e13ee7ad 791 peerDigestReqFinish(fetch, buf, 1, 1, 1, reason, 1);
792}
793
794/* complete the digest transfer, update stats, unlock/release everything */
795static void
796peerDigestReqFinish(DigestFetchState * fetch, char *buf,
62e76326 797 int fcb_valid, int pdcb_valid, int pcb_valid,
798 const char *reason, int err)
e13ee7ad 799{
800 assert(reason);
801
802 /* must go before peerDigestPDFinish */
62e76326 803
e13ee7ad 804 if (pdcb_valid) {
be4d35dc 805 fetch->pd->flags.requested = false;
62e76326 806 fetch->pd->req_result = reason;
e13ee7ad 807 }
62e76326 808
e13ee7ad 809 /* schedule next check if peer is still out there */
810 if (pcb_valid) {
62e76326 811 PeerDigest *pd = fetch->pd;
812
813 if (err) {
814 pd->times.retry_delay = peerDigestIncDelay(pd);
815 peerDigestSetCheck(pd, pd->times.retry_delay);
816 } else {
817 pd->times.retry_delay = 0;
818 peerDigestSetCheck(pd, peerDigestNewDelay(fetch->entry));
819 }
9b7de833 820 }
62e76326 821
e13ee7ad 822 /* note: order is significant */
823 if (fcb_valid)
62e76326 824 peerDigestFetchSetStats(fetch);
825
e13ee7ad 826 if (pdcb_valid)
62e76326 827 peerDigestPDFinish(fetch, pcb_valid, err);
828
e13ee7ad 829 if (fcb_valid)
62e76326 830 peerDigestFetchFinish(fetch, err);
9b7de833 831}
832
e13ee7ad 833/* destroys digest if peer disappeared
834 * must be called only when fetch and pd cbdata are valid */
9b7de833 835static void
e13ee7ad 836peerDigestPDFinish(DigestFetchState * fetch, int pcb_valid, int err)
9b7de833 837{
e13ee7ad 838 PeerDigest *pd = fetch->pd;
0e3b8c9f 839 const auto host = pd->host;
e13ee7ad 840 pd->times.received = squid_curtime;
841 pd->times.req_delay = fetch->resp_time;
a0864754
AJ
842 pd->stats.sent.kbytes += fetch->sent.bytes;
843 pd->stats.recv.kbytes += fetch->recv.bytes;
e13ee7ad 844 pd->stats.sent.msgs += fetch->sent.msg;
845 pd->stats.recv.msgs += fetch->recv.msg;
846
847 if (err) {
e0236918 848 debugs(72, DBG_IMPORTANT, "" << (pcb_valid ? "temporary " : "" ) << "disabling (" << pd->req_result << ") digest from " << host);
62e76326 849
a901d0b4
AJ
850 delete pd->cd;
851 pd->cd = nullptr;
62e76326 852
be4d35dc 853 pd->flags.usable = false;
62e76326 854
855 if (!pcb_valid)
856 peerDigestNotePeerGone(pd);
e13ee7ad 857 } else {
62e76326 858 assert(pcb_valid);
859
be4d35dc 860 pd->flags.usable = true;
e13ee7ad 861
62e76326 862 /* XXX: ugly condition, but how? */
e13ee7ad 863
62e76326 864 if (fetch->entry->store_status == STORE_OK)
bf8fe701 865 debugs(72, 2, "re-used old digest from " << host);
62e76326 866 else
bf8fe701 867 debugs(72, 2, "received valid digest from " << host);
d1cdaa16 868 }
62e76326 869
fa80a8ef 870 cbdataReferenceDone(fetch->pd);
e13ee7ad 871}
872
873/* free fetch state structures
874 * must be called only when fetch cbdata is valid */
875static void
876peerDigestFetchFinish(DigestFetchState * fetch, int err)
877{
878 assert(fetch->entry && fetch->request);
879
9b7de833 880 if (fetch->old_entry) {
48e7baac 881 debugs(72, 3, "peerDigestFetchFinish: deleting old entry");
8121ba82 882 storeUnregister(fetch->old_sc, fetch->old_entry, fetch);
d88e3c49 883 fetch->old_entry->releaseRequest();
acc5dc4c 884 fetch->old_entry->unlock("peerDigestFetchFinish old");
62e76326 885 fetch->old_entry = NULL;
9b7de833 886 }
62e76326 887
1543ab6c 888 /* update global stats */
a0864754
AJ
889 statCounter.cd.kbytes_sent += fetch->sent.bytes;
890 statCounter.cd.kbytes_recv += fetch->recv.bytes;
83704487 891 statCounter.cd.msgs_sent += fetch->sent.msg;
892 statCounter.cd.msgs_recv += fetch->recv.msg;
e13ee7ad 893
a44986dc 894 delete fetch;
9b7de833 895}
896
e13ee7ad 897/* calculate fetch stats after completion */
898static void
899peerDigestFetchSetStats(DigestFetchState * fetch)
900{
901 MemObject *mem;
902 assert(fetch->entry && fetch->request);
903
904 mem = fetch->entry->mem_obj;
905 assert(mem);
906
907 /* XXX: outgoing numbers are not precise */
908 /* XXX: we must distinguish between 304 hits and misses here */
e11fe29a 909 fetch->sent.bytes = fetch->request->prefixLen();
528b2c61 910 /* XXX: this is slightly wrong: we don't KNOW that the entire memobject
911 * was fetched. We only know how big it is
912 */
913 fetch->recv.bytes = mem->size();
e13ee7ad 914 fetch->sent.msg = fetch->recv.msg = 1;
915 fetch->expires = fetch->entry->expires;
916 fetch->resp_time = squid_curtime - fetch->start_time;
917
bf8fe701 918 debugs(72, 3, "peerDigestFetchFinish: recv " << fetch->recv.bytes <<
919 " bytes in " << (int) fetch->resp_time << " secs");
920
921 debugs(72, 3, "peerDigestFetchFinish: expires: " <<
922 (long int) fetch->expires << " (" << std::showpos <<
923 (int) (fetch->expires - squid_curtime) << "), lmt: " <<
438b41ba
EB
924 std::noshowpos << (long int) fetch->entry->lastModified() << " (" <<
925 std::showpos << (int) (fetch->entry->lastModified() - squid_curtime) <<
bf8fe701 926 ")");
927
e13ee7ad 928}
929
9b7de833 930static int
8a6218c6 931peerDigestSetCBlock(PeerDigest * pd, const char *buf)
9b7de833 932{
933 StoreDigestCBlock cblock;
934 int freed_size = 0;
0e3b8c9f 935 const auto host = pd->host;
e13ee7ad 936
41d00cd3 937 memcpy(&cblock, buf, sizeof(cblock));
9b7de833 938 /* network -> host conversions */
939 cblock.ver.current = ntohs(cblock.ver.current);
940 cblock.ver.required = ntohs(cblock.ver.required);
941 cblock.capacity = ntohl(cblock.capacity);
942 cblock.count = ntohl(cblock.count);
943 cblock.del_count = ntohl(cblock.del_count);
944 cblock.mask_size = ntohl(cblock.mask_size);
bf8fe701 945 debugs(72, 2, "got digest cblock from " << host << "; ver: " <<
946 (int) cblock.ver.current << " (req: " << (int) cblock.ver.required <<
947 ")");
948
26ac0430
AJ
949 debugs(72, 2, "\t size: " <<
950 cblock.mask_size << " bytes, e-cnt: " <<
951 cblock.count << ", e-util: " <<
bf8fe701 952 xpercentInt(cblock.count, cblock.capacity) << "%" );
6106c6fc 953 /* check version requirements (both ways) */
62e76326 954
9b7de833 955 if (cblock.ver.required > CacheDigestVer.current) {
e0236918 956 debugs(72, DBG_IMPORTANT, "" << host << " digest requires version " <<
bf8fe701 957 cblock.ver.required << "; have: " << CacheDigestVer.current);
958
62e76326 959 return 0;
9b7de833 960 }
62e76326 961
6106c6fc 962 if (cblock.ver.current < CacheDigestVer.required) {
e0236918 963 debugs(72, DBG_IMPORTANT, "" << host << " digest is version " <<
bf8fe701 964 cblock.ver.current << "; we require: " <<
965 CacheDigestVer.required);
966
62e76326 967 return 0;
6106c6fc 968 }
62e76326 969
9b7de833 970 /* check consistency */
4b4cd312 971 if (cblock.ver.required > cblock.ver.current ||
62e76326 972 cblock.mask_size <= 0 || cblock.capacity <= 0 ||
973 cblock.bits_per_entry <= 0 || cblock.hash_func_count <= 0) {
fa84c01d 974 debugs(72, DBG_CRITICAL, "" << host << " digest cblock is corrupted.");
62e76326 975 return 0;
9b7de833 976 }
62e76326 977
d1cdaa16 978 /* check consistency further */
5bc5e81f 979 if ((size_t)cblock.mask_size != CacheDigest::CalcMaskSize(cblock.capacity, cblock.bits_per_entry)) {
fa84c01d 980 debugs(72, DBG_CRITICAL, host << " digest cblock is corrupted " <<
e4049756 981 "(mask size mismatch: " << cblock.mask_size << " ? " <<
5bc5e81f 982 CacheDigest::CalcMaskSize(cblock.capacity, cblock.bits_per_entry)
e4049756 983 << ").");
62e76326 984 return 0;
d1cdaa16 985 }
62e76326 986
d1cdaa16 987 /* there are some things we cannot do yet */
988 if (cblock.hash_func_count != CacheDigestHashFuncCount) {
fa84c01d 989 debugs(72, DBG_CRITICAL, "" << host << " digest: unsupported #hash functions: " <<
bf8fe701 990 cblock.hash_func_count << " ? " << CacheDigestHashFuncCount << ".");
62e76326 991 return 0;
d1cdaa16 992 }
62e76326 993
9b7de833 994 /*
995 * no cblock bugs below this point
996 */
997 /* check size changes */
e6ccf245 998 if (pd->cd && cblock.mask_size != (ssize_t)pd->cd->mask_size) {
e4049756 999 debugs(72, 2, host << " digest changed size: " << cblock.mask_size <<
1000 " -> " << pd->cd->mask_size);
62e76326 1001 freed_size = pd->cd->mask_size;
a901d0b4
AJ
1002 delete pd->cd;
1003 pd->cd = nullptr;
9b7de833 1004 }
62e76326 1005
e13ee7ad 1006 if (!pd->cd) {
26ac0430
AJ
1007 debugs(72, 2, "creating " << host << " digest; size: " << cblock.mask_size << " (" <<
1008 std::showpos << (int) (cblock.mask_size - freed_size) << ") bytes");
e04fc9d3 1009 pd->cd = new CacheDigest(cblock.capacity, cblock.bits_per_entry);
62e76326 1010
1011 if (cblock.mask_size >= freed_size)
a0864754 1012 statCounter.cd.memory += (cblock.mask_size - freed_size);
9b7de833 1013 }
62e76326 1014
e13ee7ad 1015 assert(pd->cd);
9b7de833 1016 /* these assignments leave us in an inconsistent state until we finish reading the digest */
e13ee7ad 1017 pd->cd->count = cblock.count;
1018 pd->cd->del_count = cblock.del_count;
9b7de833 1019 return 1;
1020}
1021
9b7de833 1022static int
8a6218c6 1023peerDigestUseful(const PeerDigest * pd)
9b7de833 1024{
d1cdaa16 1025 /* TODO: we should calculate the prob of a false hit instead of bit util */
6fbd1d41 1026 const auto bit_util = pd->cd->usedMaskPercent();
62e76326 1027
6fbd1d41 1028 if (bit_util > 65.0) {
fa84c01d 1029 debugs(72, DBG_CRITICAL, "Warning: " << pd->host <<
6fbd1d41 1030 " peer digest has too many bits on (" << bit_util << "%).");
62e76326 1031 return 0;
9b7de833 1032 }
62e76326 1033
9b7de833 1034 return 1;
1035}
7f6eb0fe 1036
e13ee7ad 1037static int
1038saneDiff(time_t diff)
1039{
3db6fb54 1040 return abs((int) diff) > squid_curtime / 2 ? 0 : diff;
e13ee7ad 1041}
1042
1043void
8a6218c6 1044peerDigestStatsReport(const PeerDigest * pd, StoreEntry * e)
e13ee7ad 1045{
1046#define f2s(flag) (pd->flags.flag ? "yes" : "no")
38650cc8 1047#define appendTime(tm) storeAppendPrintf(e, "%s\t %10ld\t %+d\t %+d\n", \
1048 ""#tm, (long int)pd->times.tm, \
e13ee7ad 1049 saneDiff(pd->times.tm - squid_curtime), \
1050 saneDiff(pd->times.tm - pd->times.initialized))
1051
e13ee7ad 1052 assert(pd);
1053
0e3b8c9f
AJ
1054 auto host = pd->host;
1055 storeAppendPrintf(e, "\npeer digest from " SQUIDSBUFPH "\n", SQUIDSBUFPRINT(host));
e13ee7ad 1056
1057 cacheDigestGuessStatsReport(&pd->stats.guess, e, host);
1058
1059 storeAppendPrintf(e, "\nevent\t timestamp\t secs from now\t secs from init\n");
1060 appendTime(initialized);
1061 appendTime(needed);
1062 appendTime(requested);
1063 appendTime(received);
1064 appendTime(next_check);
1065
1066 storeAppendPrintf(e, "peer digest state:\n");
1067 storeAppendPrintf(e, "\tneeded: %3s, usable: %3s, requested: %3s\n",
62e76326 1068 f2s(needed), f2s(usable), f2s(requested));
e13ee7ad 1069 storeAppendPrintf(e, "\n\tlast retry delay: %d secs\n",
62e76326 1070 (int) pd->times.retry_delay);
e13ee7ad 1071 storeAppendPrintf(e, "\tlast request response time: %d secs\n",
62e76326 1072 (int) pd->times.req_delay);
e13ee7ad 1073 storeAppendPrintf(e, "\tlast request result: %s\n",
62e76326 1074 pd->req_result ? pd->req_result : "(none)");
e13ee7ad 1075
1076 storeAppendPrintf(e, "\npeer digest traffic:\n");
1077 storeAppendPrintf(e, "\trequests sent: %d, volume: %d KB\n",
62e76326 1078 pd->stats.sent.msgs, (int) pd->stats.sent.kbytes.kb);
e13ee7ad 1079 storeAppendPrintf(e, "\treplies recv: %d, volume: %d KB\n",
62e76326 1080 pd->stats.recv.msgs, (int) pd->stats.recv.kbytes.kb);
e13ee7ad 1081
1082 storeAppendPrintf(e, "\npeer digest structure:\n");
62e76326 1083
e13ee7ad 1084 if (pd->cd)
62e76326 1085 cacheDigestReport(pd->cd, host, e);
e13ee7ad 1086 else
62e76326 1087 storeAppendPrintf(e, "\tno in-memory copy\n");
e13ee7ad 1088}
1089
7f6eb0fe 1090#endif
f53969cc 1091