aa7c141806b033b61a141b328b59f56047a04e8a
[ep-engine.git] / src / hash_table.cc
1 /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3  *     Copyright 2016 Couchbase, Inc
4  *
5  *   Licensed under the Apache License, Version 2.0 (the "License");
6  *   you may not use this file except in compliance with the License.
7  *   You may obtain a copy of the License at
8  *
9  *       http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *   Unless required by applicable law or agreed to in writing, software
12  *   distributed under the License is distributed on an "AS IS" BASIS,
13  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *   See the License for the specific language governing permissions and
15  *   limitations under the License.
16  */
17
18 #include "hash_table.h"
19
20 #include "stored_value_factories.h"
21
22 #include <cstring>
23
24 #ifndef DEFAULT_HT_SIZE
25 #define DEFAULT_HT_SIZE 1531
26 #endif
27
28 size_t HashTable::defaultNumBuckets = DEFAULT_HT_SIZE;
29 size_t HashTable::defaultNumLocks = 193;
30
31 static ssize_t prime_size_table[] = {
32     3, 7, 13, 23, 47, 97, 193, 383, 769, 1531, 3079, 6143, 12289, 24571, 49157,
33     98299, 196613, 393209, 786433, 1572869, 3145721, 6291449, 12582917,
34     25165813, 50331653, 100663291, 201326611, 402653189, 805306357,
35     1610612741, -1
36 };
37
38
39 std::ostream& operator<<(std::ostream& os, const HashTable::Position& pos) {
40     os << "{lock:" << pos.lock << " bucket:" << pos.hash_bucket << "/" << pos.ht_size << "}";
41     return os;
42 }
43
44 HashTable::HashTable(EPStats& st,
45                      std::unique_ptr<AbstractStoredValueFactory> svFactory,
46                      size_t s, size_t l)
47     : maxDeletedRevSeqno(0),
48       numTotalItems(0),
49       numNonResidentItems(0),
50       numDeletedItems(0),
51       datatypeCounts(),
52       numEjects(0),
53       memSize(0),
54       cacheSize(0),
55       metaDataMemory(0),
56       stats(st),
57       valFact(std::move(svFactory)),
58       visitors(0),
59       numItems(0),
60       numResizes(0),
61       numTempItems(0) {
62     size = HashTable::getNumBuckets(s);
63     n_locks = HashTable::getNumLocks(l);
64     values.resize(size);
65     mutexes = new std::mutex[n_locks];
66     activeState = true;
67 }
68
69 HashTable::~HashTable() {
70     clear(true);
71     // Wait for any outstanding visitors to finish.
72     while (visitors > 0) {
73 #ifdef _MSC_VER
74         Sleep(1);
75 #else
76         usleep(100);
77 #endif
78     }
79     delete []mutexes;
80 }
81
82 void HashTable::clear(bool deactivate) {
83     if (!deactivate) {
84         // If not deactivating, assert we're already active.
85         if (!isActive()) {
86             throw std::logic_error("HashTable::clear: Cannot call on a "
87                     "non-active object");
88         }
89     }
90     MultiLockHolder mlh(mutexes, n_locks);
91     if (deactivate) {
92         setActiveState(false);
93     }
94     size_t clearedMemSize = 0;
95     size_t clearedValSize = 0;
96     for (int i = 0; i < (int)size; i++) {
97         while (values[i]) {
98             // Take ownership of the StoredValue from the vector, update
99             // statistics and release it.
100             auto v = std::move(values[i]);
101             clearedMemSize += v->size();
102             clearedValSize += v->valuelen();
103             values[i] = std::move(v->next);
104         }
105     }
106
107     stats.currentSize.fetch_sub(clearedMemSize - clearedValSize);
108
109     datatypeCounts.fill(0);
110     numTotalItems.store(0);
111     numItems.store(0);
112     numTempItems.store(0);
113     numNonResidentItems.store(0);
114     memSize.store(0);
115     cacheSize.store(0);
116 }
117
118 static size_t distance(size_t a, size_t b) {
119     return std::max(a, b) - std::min(a, b);
120 }
121
122 static size_t nearest(size_t n, size_t a, size_t b) {
123     return (distance(n, a) < distance(b, n)) ? a : b;
124 }
125
126 static bool isCurrently(size_t size, ssize_t a, ssize_t b) {
127     ssize_t current(static_cast<ssize_t>(size));
128     return (current == a || current == b);
129 }
130
131 void HashTable::resize() {
132     size_t ni = getNumInMemoryItems();
133     int i(0);
134     size_t new_size(0);
135
136     // Figure out where in the prime table we are.
137     ssize_t target(static_cast<ssize_t>(ni));
138     for (i = 0; prime_size_table[i] > 0 && prime_size_table[i] < target; ++i) {
139         // Just looking...
140     }
141
142     if (prime_size_table[i] == -1) {
143         // We're at the end, take the biggest
144         new_size = prime_size_table[i-1];
145     } else if (prime_size_table[i] < static_cast<ssize_t>(defaultNumBuckets)) {
146         // Was going to be smaller than the configured ht_size.
147         new_size = defaultNumBuckets;
148     } else if (0 == i) {
149         new_size = prime_size_table[i];
150     }else if (isCurrently(size, prime_size_table[i-1], prime_size_table[i])) {
151         // If one of the candidate sizes is the current size, maintain
152         // the current size in order to remain stable.
153         new_size = size;
154     } else {
155         // Somewhere in the middle, use the one we're closer to.
156         new_size = nearest(ni, prime_size_table[i-1], prime_size_table[i]);
157     }
158
159     resize(new_size);
160 }
161
162 void HashTable::resize(size_t newSize) {
163     if (!isActive()) {
164         throw std::logic_error("HashTable::resize: Cannot call on a "
165                 "non-active object");
166     }
167
168     // Due to the way hashing works, we can't fit anything larger than
169     // an int.
170     if (newSize > static_cast<size_t>(std::numeric_limits<int>::max())) {
171         return;
172     }
173
174     // Don't resize to the same size, either.
175     if (newSize == size) {
176         return;
177     }
178
179     MultiLockHolder mlh(mutexes, n_locks);
180     if (visitors.load() > 0) {
181         // Do not allow a resize while any visitors are actually
182         // processing.  The next attempt will have to pick it up.  New
183         // visitors cannot start doing meaningful work (we own all
184         // locks at this point).
185         return;
186     }
187
188     // Get a place for the new items.
189     table_type newValues(newSize);
190
191     stats.memOverhead->fetch_sub(memorySize());
192     ++numResizes;
193
194     // Set the new size so all the hashy stuff works.
195     size_t oldSize = size;
196     size.store(newSize);
197
198     // Move existing records into the new space.
199     for (size_t i = 0; i < oldSize; i++) {
200         while (values[i]) {
201             // unlink the front element from the hash chain at values[i].
202             auto v = std::move(values[i]);
203             values[i] = std::move(v->next);
204
205             // And re-link it into the correct place in newValues.
206             int newBucket = getBucketForHash(v->getKey().hash());
207             v->next = std::move(newValues[newBucket]);
208             newValues[newBucket] = std::move(v);
209         }
210     }
211
212     // Finally assign the new table to values.
213     values = std::move(newValues);
214
215     stats.memOverhead->fetch_add(memorySize());
216 }
217
218 StoredValue* HashTable::find(const DocKey& key,
219                              TrackReference trackReference,
220                              WantsDeleted wantsDeleted) {
221     if (!isActive()) {
222         throw std::logic_error("HashTable::find: Cannot call on a "
223                 "non-active object");
224     }
225     HashBucketLock hbl = getLockedBucket(key);
226     return unlocked_find(key, hbl.getBucketNum(), wantsDeleted, trackReference);
227 }
228
229 std::unique_ptr<Item> HashTable::getRandomKey(long rnd) {
230     /* Try to locate a partition */
231     size_t start = rnd % size;
232     size_t curr = start;
233     std::unique_ptr<Item> ret;
234
235     do {
236         ret = getRandomKeyFromSlot(curr++);
237         if (curr == size) {
238             curr = 0;
239         }
240     } while (ret == NULL && curr != start);
241
242     return ret;
243 }
244
245 MutationStatus HashTable::set(Item& val) {
246     if (!StoredValue::hasAvailableSpace(stats, val, false)) {
247         return MutationStatus::NoMem;
248     }
249
250     HashBucketLock hbl = getLockedBucket(val.getKey());
251     StoredValue* v = unlocked_find(val.getKey(),
252                                    hbl.getBucketNum(),
253                                    WantsDeleted::Yes,
254                                    TrackReference::No);
255     if (v) {
256         return unlocked_updateStoredValue(hbl.getHTLock(), *v, val);
257     } else {
258         unlocked_addNewStoredValue(hbl, val);
259         return MutationStatus::WasClean;
260     }
261 }
262
263 MutationStatus HashTable::unlocked_updateStoredValue(
264         const std::unique_lock<std::mutex>& htLock,
265         StoredValue& v,
266         const Item& itm) {
267     if (!htLock) {
268         throw std::invalid_argument(
269                 "HashTable::unlocked_updateStoredValue: htLock "
270                 "not held");
271     }
272
273     if (!isActive()) {
274         throw std::logic_error(
275                 "HashTable::unlocked_updateStoredValue: Cannot "
276                 "call on a non-active HT object");
277     }
278
279     MutationStatus status =
280             v.isDirty() ? MutationStatus::WasDirty : MutationStatus::WasClean;
281     if (!v.isResident() && !v.isDeleted() && !v.isTempItem()) {
282         decrNumNonResidentItems();
283     }
284
285     if (v.isTempItem()) {
286         --numTempItems;
287         ++numItems;
288         ++numTotalItems;
289     }
290
291     if (v.isDeleted() && !itm.isDeleted()) {
292         --numDeletedItems;
293     }
294     if (!v.isDeleted() && itm.isDeleted()) {
295         ++numDeletedItems;
296     }
297
298     // If the item we are replacing is resident then we need to make sure we
299     // appropriately alter the datatype stats.
300     if (v.getDatatype() != itm.getDataType()) {
301         --datatypeCounts[v.getDatatype()];
302         ++datatypeCounts[itm.getDataType()];
303     }
304
305     /* setValue() will mark v as undeleted if required */
306     v.setValue(itm, *this);
307     return status;
308 }
309
310 StoredValue* HashTable::unlocked_addNewStoredValue(const HashBucketLock& hbl,
311                                                    const Item& itm) {
312     if (!hbl.getHTLock()) {
313         throw std::invalid_argument(
314                 "HashTable::unlocked_addNewStoredValue: htLock "
315                 "not held");
316     }
317
318     if (!isActive()) {
319         throw std::invalid_argument(
320                 "HashTable::unlocked_addNewStoredValue: Cannot "
321                 "call on a non-active HT object");
322     }
323
324     // Create a new StoredValue and link it into the head of the bucket chain.
325     auto v = (*valFact)(itm, std::move(values[hbl.getBucketNum()]), *this);
326     if (v->isTempItem()) {
327         ++numTempItems;
328     } else {
329         ++numItems;
330         ++numTotalItems;
331         ++datatypeCounts[v->getDatatype()];
332     }
333     values[hbl.getBucketNum()] = std::move(v);
334
335     return values[hbl.getBucketNum()].get();
336 }
337
338 std::pair<StoredValue*, StoredValue::UniquePtr>
339 HashTable::unlocked_replaceByCopy(const HashBucketLock& hbl,
340                                   const StoredValue& vToCopy) {
341     if (!hbl.getHTLock()) {
342         throw std::invalid_argument(
343                 "HashTable::unlocked_replaceByCopy: htLock "
344                 "not held");
345     }
346
347     if (!isActive()) {
348         throw std::invalid_argument(
349                 "HashTable::unlocked_replaceByCopy: Cannot "
350                 "call on a non-active HT object");
351     }
352
353     /* Release (remove) the StoredValue from the hash table */
354     auto releasedSv = unlocked_release(hbl, vToCopy.getKey());
355
356     /* Copy the StoredValue and link it into the head of the bucket chain. */
357     auto newSv = valFact->copyStoredValue(
358             vToCopy, std::move(values[hbl.getBucketNum()]), *this);
359     if (newSv->isTempItem()) {
360         ++numTempItems;
361     } else {
362         ++numItems;
363         ++numTotalItems;
364     }
365     values[hbl.getBucketNum()] = std::move(newSv);
366
367     return {values[hbl.getBucketNum()].get(), std::move(releasedSv)};
368 }
369
370 void HashTable::unlocked_softDelete(const std::unique_lock<std::mutex>& htLock,
371                                     StoredValue& v,
372                                     bool onlyMarkDeleted) {
373     if (!v.isResident() && !v.isDeleted() && !v.isTempItem()) {
374         decrNumNonResidentItems();
375     }
376
377     --datatypeCounts[v.getDatatype()];
378
379     if (onlyMarkDeleted) {
380         v.markDeleted();
381     } else {
382         if (v.isTempItem()) {
383             --numTempItems;
384             ++numItems;
385             ++numTotalItems;
386         }
387         v.del(*this);
388     }
389     ++numDeletedItems;
390 }
391
392 StoredValue* HashTable::unlocked_find(const DocKey& key,
393                                       int bucket_num,
394                                       WantsDeleted wantsDeleted,
395                                       TrackReference trackReference) {
396     for (StoredValue* v = values[bucket_num].get(); v; v = v->next.get()) {
397         if (v->hasKey(key)) {
398             if (trackReference == TrackReference::Yes && !v->isDeleted()) {
399                 v->referenced();
400             }
401             if (wantsDeleted == WantsDeleted::Yes || !v->isDeleted()) {
402                 return v;
403             } else {
404                 return NULL;
405             }
406         }
407     }
408     return NULL;
409 }
410
411 void HashTable::unlocked_del(const HashBucketLock& hbl, const DocKey& key) {
412     unlocked_release(hbl, key).reset();
413 }
414
415 StoredValue::UniquePtr HashTable::unlocked_release(
416         const HashBucketLock& hbl, const DocKey& key) {
417     if (!hbl.getHTLock()) {
418         throw std::invalid_argument(
419                 "HashTable::unlocked_remove: htLock "
420                 "not held");
421     }
422
423     if (!isActive()) {
424         throw std::logic_error(
425                 "HashTable::unlocked_remove: Cannot call on a "
426                 "non-active object");
427     }
428
429     // Remove the first (should only be one) StoredValue with the given key.
430     auto released = hashChainRemoveFirst(
431             values[hbl.getBucketNum()],
432             [key](const StoredValue* v) { return v->hasKey(key); });
433
434     if (!released) {
435         /* We shouldn't reach here, we must delete the StoredValue in the
436            HashTable */
437         throw std::logic_error(
438                 "HashTable::unlocked_del: StoredValue to be deleted "
439                 "not found in HashTable; possibly HashTable leak");
440     }
441
442     // Update statistics now the item has been removed.
443     StoredValue::reduceCacheSize(*this, released->size());
444     StoredValue::reduceMetaDataSize(*this, stats, released->metaDataSize());
445     if (released->isTempItem()) {
446         --numTempItems;
447     } else {
448         decrNumItems();
449         decrNumTotalItems();
450         --datatypeCounts[released->getDatatype()];
451         if (released->isDeleted()) {
452             --numDeletedItems;
453         }
454     }
455     return released;
456 }
457
458 void HashTable::visit(HashTableVisitor &visitor) {
459     if ((numItems.load() + numTempItems.load()) == 0 || !isActive()) {
460         return;
461     }
462
463     // Acquire one (any) of the mutexes before incrementing {visitors}, this
464     // prevents any race between this visitor and the HashTable resizer.
465     // See comments in pauseResumeVisit() for further details.
466     std::unique_lock<std::mutex> lh(mutexes[0]);
467     VisitorTracker vt(&visitors);
468     lh.unlock();
469
470     bool aborted = !visitor.shouldContinue();
471     size_t visited = 0;
472     for (int l = 0; isActive() && !aborted && l < static_cast<int>(n_locks);
473          l++) {
474         for (int i = l; i < static_cast<int>(size); i+= n_locks) {
475             // (re)acquire mutex on each HashBucket, to minimise any impact
476             // on front-end threads.
477             HashBucketLock lh(i, mutexes[l]);
478
479             StoredValue* v = values[i].get();
480             if (v) {
481                 // TODO: Perf: This check seems costly - do we think it's still
482                 // worth keeping?
483                 auto hashbucket = getBucketForHash(v->getKey().hash());
484                 if (i != hashbucket) {
485                     throw std::logic_error("HashTable::visit: inconsistency "
486                             "between StoredValue's calculated hashbucket "
487                             "(which is " + std::to_string(hashbucket) +
488                             ") and bucket is is located in (which is " +
489                             std::to_string(i) + ")");
490                 }
491             }
492             while (v) {
493                 StoredValue* tmp = v->next.get();
494                 visitor.visit(lh, v);
495                 v = tmp;
496             }
497             ++visited;
498         }
499         aborted = !visitor.shouldContinue();
500     }
501 }
502
503 void HashTable::visitDepth(HashTableDepthVisitor &visitor) {
504     if (numItems.load() == 0 || !isActive()) {
505         return;
506     }
507     size_t visited = 0;
508     VisitorTracker vt(&visitors);
509
510     for (int l = 0; l < static_cast<int>(n_locks); l++) {
511         LockHolder lh(mutexes[l]);
512         for (int i = l; i < static_cast<int>(size); i+= n_locks) {
513             size_t depth = 0;
514             StoredValue* p = values[i].get();
515             if (p) {
516                 // TODO: Perf: This check seems costly - do we think it's still
517                 // worth keeping?
518                 auto hashbucket = getBucketForHash(p->getKey().hash());
519                 if (i != hashbucket) {
520                     throw std::logic_error("HashTable::visit: inconsistency "
521                             "between StoredValue's calculated hashbucket "
522                             "(which is " + std::to_string(hashbucket) +
523                             ") and bucket it is located in (which is " +
524                             std::to_string(i) + ")");
525                 }
526             }
527             size_t mem(0);
528             while (p) {
529                 depth++;
530                 mem += p->size();
531                 p = p->next.get();
532             }
533             visitor.visit(i, depth, mem);
534             ++visited;
535         }
536     }
537 }
538
539 HashTable::Position
540 HashTable::pauseResumeVisit(PauseResumeHashTableVisitor& visitor,
541                             Position& start_pos) {
542     if ((numItems.load() + numTempItems.load()) == 0 || !isActive()) {
543         // Nothing to visit
544         return endPosition();
545     }
546
547     bool paused = false;
548
549     // To attempt to minimize the impact the visitor has on normal frontend
550     // operations, we deliberately acquire (and release) the mutex between
551     // each hash_bucket - see `lh` in the inner for() loop below. This means we
552     // hold a given mutex for a large number of short durations, instead of just
553     // one single, long duration.
554     // *However*, there is a potential race with this approach - the {size} of
555     // the HashTable may be changed (by the Resizer task) between us first
556     // reading it to calculate the starting hash_bucket, and then reading it
557     // inside the inner for() loop. To prevent this race, we explicitly acquire
558     // (any) mutex, increment {visitors} and then release the mutex. This
559     //avoids the race as if visitors >0 then Resizer will not attempt to resize.
560     std::unique_lock<std::mutex> lh(mutexes[0]);
561     VisitorTracker vt(&visitors);
562     lh.unlock();
563
564     // Start from the requested lock number if in range.
565     size_t lock = (start_pos.lock < n_locks) ? start_pos.lock : 0;
566     size_t hash_bucket = 0;
567
568     for (; isActive() && !paused && lock < n_locks; lock++) {
569
570         // If the bucket position is *this* lock, then start from the
571         // recorded bucket (as long as we haven't resized).
572         hash_bucket = lock;
573         if (start_pos.lock == lock &&
574             start_pos.ht_size == size &&
575             start_pos.hash_bucket < size) {
576             hash_bucket = start_pos.hash_bucket;
577         }
578
579         // Iterate across all values in the hash buckets owned by this lock.
580         // Note: we don't record how far into the bucket linked-list we
581         // pause at; so any restart will begin from the next bucket.
582         for (; !paused && hash_bucket < size; hash_bucket += n_locks) {
583             LockHolder lh(mutexes[lock]);
584
585             StoredValue* v = values[hash_bucket].get();
586             while (!paused && v) {
587                 StoredValue* tmp = v->next.get();
588                 paused = !visitor.visit(*v);
589                 v = tmp;
590             }
591         }
592
593         // If the visitor paused us before we visited all hash buckets owned
594         // by this lock, we don't want to skip the remaining hash buckets, so
595         // stop the outer for loop from advancing to the next lock.
596         if (paused && hash_bucket < size) {
597             break;
598         }
599
600         // Finished all buckets owned by this lock. Set hash_bucket to 'size'
601         // to give a consistent marker for "end of lock".
602         hash_bucket = size;
603     }
604
605     // Return the *next* location that should be visited.
606     return HashTable::Position(size, lock, hash_bucket);
607 }
608
609 HashTable::Position HashTable::endPosition() const  {
610     return HashTable::Position(size, n_locks, size);
611 }
612
613 static inline size_t getDefault(size_t x, size_t d) {
614     return x == 0 ? d : x;
615 }
616
617 size_t HashTable::getNumBuckets(size_t n) {
618     return getDefault(n, defaultNumBuckets);
619 }
620
621 size_t HashTable::getNumLocks(size_t n) {
622     return getDefault(n, defaultNumLocks);
623 }
624
625 void HashTable::setDefaultNumBuckets(size_t to) {
626     if (to != 0) {
627         defaultNumBuckets = to;
628     }
629 }
630
631 void HashTable::setDefaultNumLocks(size_t to) {
632     if (to != 0) {
633         defaultNumLocks = to;
634     }
635 }
636
637 bool HashTable::unlocked_ejectItem(StoredValue*& vptr,
638                                    item_eviction_policy_t policy) {
639     if (vptr == nullptr) {
640         throw std::invalid_argument("HashTable::unlocked_ejectItem: "
641                 "Unable to delete NULL StoredValue");
642     }
643     if (policy == VALUE_ONLY) {
644         bool rv = vptr->ejectValue(*this, policy);
645         if (rv) {
646             ++stats.numValueEjects;
647             ++numNonResidentItems;
648             ++numEjects;
649             return true;
650         } else {
651             ++stats.numFailedEjects;
652             return false;
653         }
654     } else { // full eviction.
655         if (vptr->eligibleForEviction(policy)) {
656             StoredValue::reduceMetaDataSize(*this, stats,
657                                             vptr->metaDataSize());
658             StoredValue::reduceCacheSize(*this, vptr->size());
659             int bucket_num = getBucketForHash(vptr->getKey().hash());
660
661             // Remove the item from the hash table.
662             auto removed = hashChainRemoveFirst(
663                     values[bucket_num],
664                     [vptr](const StoredValue* v) { return v == vptr; });
665
666             if (removed->isResident()) {
667                 ++stats.numValueEjects;
668             }
669             if (!removed->isResident() && !removed->isTempItem()) {
670                 decrNumNonResidentItems(); // Decrement because the item is
671                                            // fully evicted.
672             }
673             decrNumItems(); // Decrement because the item is fully evicted.
674             --datatypeCounts[vptr->getDatatype()];
675             ++numEjects;
676             updateMaxDeletedRevSeqno(vptr->getRevSeqno());
677
678             return true;
679         } else {
680             ++stats.numFailedEjects;
681             return false;
682         }
683     }
684 }
685
686 std::unique_ptr<Item> HashTable::getRandomKeyFromSlot(int slot) {
687     auto lh = getLockedBucket(slot);
688     for (StoredValue* v = values[slot].get(); v; v = v->next.get()) {
689         if (!v->isTempItem() && !v->isDeleted() && v->isResident()) {
690             return v->toItem(false, 0);
691         }
692     }
693
694     return nullptr;
695 }
696
697 bool HashTable::unlocked_restoreValue(
698         const std::unique_lock<std::mutex>& htLock,
699         const Item& itm,
700         StoredValue& v) {
701     if (!htLock || !isActive() || v.isResident()) {
702         return false;
703     }
704
705     if (v.isTempInitialItem()) { // Regular item with the full eviction
706         --numTempItems;
707         ++numItems;
708         /* set it back to false as we created a temp item by setting it to true
709            when bg fetch is scheduled (full eviction mode). */
710         v.setNewCacheItem(false);
711         ++datatypeCounts[itm.getDataType()];
712     } else {
713         decrNumNonResidentItems();
714     }
715
716     v.restoreValue(itm);
717
718     StoredValue::increaseCacheSize(*this, v.getValue()->length());
719     return true;
720 }
721
722 void HashTable::unlocked_restoreMeta(const std::unique_lock<std::mutex>& htLock,
723                                      const Item& itm,
724                                      StoredValue& v) {
725     if (!htLock) {
726         throw std::invalid_argument(
727                 "HashTable::unlocked_restoreMeta: htLock "
728                 "not held");
729     }
730
731     if (!isActive()) {
732         throw std::logic_error(
733                 "HashTable::unlocked_restoreMeta: Cannot "
734                 "call on a non-active HT object");
735     }
736
737     v.restoreMeta(itm);
738     if (!itm.isDeleted()) {
739         --numTempItems;
740         ++numItems;
741         ++numNonResidentItems;
742         ++datatypeCounts[v.getDatatype()];
743     }
744 }