Loading...
Searching...
No Matches
full-producer.cpp
Go to the documentation of this file.
1/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2/*
3 * Copyright (c) 2014-2024, The University of Memphis
4 *
5 * This file is part of PSync.
6 * See AUTHORS.md for complete list of PSync authors and contributors.
7 *
8 * PSync is free software: you can redistribute it and/or modify it under the terms
9 * of the GNU Lesser General Public License as published by the Free Software Foundation,
10 * either version 3 of the License, or (at your option) any later version.
11 *
12 * PSync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 * PURPOSE. See the GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License along with
17 * PSync, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
18 */
19
22#include "PSync/detail/util.hpp"
23
24#include <ndn-cxx/lp/tags.hpp>
25#include <ndn-cxx/security/validator-null.hpp>
26#include <ndn-cxx/util/logger.hpp>
27
28#include <cstring>
29
30namespace psync {
31
32NDN_LOG_INIT(psync.FullProducer);
33
35 ndn::KeyChain& keyChain,
36 const ndn::Name& syncPrefix,
37 const Options& opts)
38 : ProducerBase(face, keyChain, opts.ibfCount, syncPrefix, opts.syncDataFreshness,
39 opts.ibfCompression, opts.contentCompression)
40 , m_syncInterestLifetime(opts.syncInterestLifetime)
41 , m_onUpdate(opts.onUpdate)
42{
43 m_registeredPrefix = m_face.setInterestFilter(ndn::InterestFilter(m_syncPrefix).allowLoopback(false),
44 [this] (auto&&... args) { onSyncInterest(std::forward<decltype(args)>(args)...); },
45 [] (auto&&... args) { onRegisterFailed(std::forward<decltype(args)>(args)...); });
46
47 // Should we do this after setInterestFilter success call back
48 // (Currently following ChronoSync's way)
49 sendSyncInterest();
50}
51
53 ndn::KeyChain& keyChain,
54 size_t expectedNumEntries,
55 const ndn::Name& syncPrefix,
56 const ndn::Name& userPrefix,
57 UpdateCallback onUpdateCb,
58 ndn::time::milliseconds syncInterestLifetime,
59 ndn::time::milliseconds syncReplyFreshness,
60 CompressionScheme ibltCompression,
61 CompressionScheme contentCompression)
62 : FullProducer(face, keyChain, syncPrefix,
63 Options{std::move(onUpdateCb), static_cast<uint32_t>(expectedNumEntries), ibltCompression,
64 syncInterestLifetime, syncReplyFreshness, contentCompression})
65{
66 addUserNode(userPrefix);
67}
68
70{
71 if (m_fetcher) {
72 m_fetcher->stop();
73 }
74}
75
76void
77FullProducer::publishName(const ndn::Name& prefix, std::optional<uint64_t> seq)
78{
79 if (m_prefixes.find(prefix) == m_prefixes.end()) {
80 NDN_LOG_WARN("Prefix not added: " << prefix);
81 return;
82 }
83
84 uint64_t newSeq = seq.value_or(m_prefixes[prefix] + 1);
85 NDN_LOG_INFO("Publish: " << prefix << "/" << newSeq);
86 updateSeqNo(prefix, newSeq);
87
88 m_inNoNewDataWaitOutPeriod = false;
89
90 satisfyPendingInterests(ndn::Name(prefix).appendNumber(newSeq));
91}
92
93void
94FullProducer::sendSyncInterest()
95{
96 if (m_inNoNewDataWaitOutPeriod) {
97 NDN_LOG_TRACE("Cannot send sync Interest as Data is expected from CS");
98 return;
99 }
100
101 // If we send two sync interest one after the other
102 // since there is no new data in the network yet,
103 // when data is available it may satisfy both of them
104 if (m_fetcher) {
105 m_fetcher->stop();
106 }
107
108 // Sync Interest format for full sync: /<sync-prefix>/<ourLatestIBF>
109 ndn::Name syncInterestName = m_syncPrefix;
110
111 // Append our latest IBF
112 m_iblt.appendToName(syncInterestName);
113 // Append cumulative updates that has been inserted into this IBF
114 syncInterestName.appendNumber(m_numOwnElements);
115
116 auto currentTime = ndn::time::system_clock::now();
117 if ((currentTime - m_lastInterestSentTime < ndn::time::milliseconds(MIN_JITTER)) &&
118 (m_outstandingInterestName == syncInterestName)) {
119 NDN_LOG_TRACE("Suppressing Interest: " << std::hash<ndn::Name>{}(syncInterestName));
120 return;
121 }
122
123 m_outstandingInterestName = syncInterestName;
124
125 m_scheduledSyncInterestId =
126 m_scheduler.schedule(m_syncInterestLifetime / 2 + ndn::time::milliseconds(m_jitter(m_rng)),
127 [this] { sendSyncInterest(); });
128
129 ndn::Interest syncInterest(syncInterestName);
130
131 using ndn::SegmentFetcher;
132 SegmentFetcher::Options options;
133 options.interestLifetime = m_syncInterestLifetime;
134 options.maxTimeout = m_syncInterestLifetime;
135 options.rttOptions.initialRto = m_syncInterestLifetime;
136
137 // This log message must be before sending the Interest through SegmentFetcher
138 // because getNonce generates a Nonce for this Interest.
139 // SegmentFetcher makes a copy of this Interest, so if we print the Nonce
140 // after, that Nonce will be different than the one seen in tshark!
141 NDN_LOG_DEBUG("sendFullSyncInterest, nonce: " << syncInterest.getNonce() <<
142 ", hash: " << std::hash<ndn::Name>{}(syncInterestName));
143
144 m_lastInterestSentTime = currentTime;
145 m_fetcher = SegmentFetcher::start(m_face, syncInterest,
146 ndn::security::getAcceptAllValidator(), options);
147
148 m_fetcher->onComplete.connect([this, syncInterest] (const ndn::ConstBufferPtr& bufferPtr) {
149 onSyncData(syncInterest, bufferPtr);
150 });
151
152 m_fetcher->afterSegmentValidated.connect([this] (const ndn::Data& data) {
153 auto tag = data.getTag<ndn::lp::IncomingFaceIdTag>();
154 if (tag) {
155 m_incomingFace = *tag;
156 }
157 else {
158 m_incomingFace = 0;
159 }
160 });
161
162 m_fetcher->onError.connect([this] (uint32_t errorCode, const std::string& msg) {
163 NDN_LOG_ERROR("Cannot fetch sync data, error: " << errorCode << ", message: " << msg);
164 // We would like to recover from errors like NoRoute NACK quicker than sync Interest timeout.
165 // We don't react to Interest timeout here as we have scheduled the next sync Interest
166 // to be sent in half the sync Interest lifetime + jitter above. So we would react to
167 // timeout before it happens.
168 if (errorCode != SegmentFetcher::ErrorCode::INTEREST_TIMEOUT) {
169 auto after = ndn::time::milliseconds(m_jitter(m_rng));
170 NDN_LOG_DEBUG("Schedule sync Interest after: " << after);
171 m_scheduledSyncInterestId = m_scheduler.schedule(after, [this] { sendSyncInterest(); });
172 }
173 });
174}
175
176void
177FullProducer::processWaitingInterests()
178{
179 NDN_LOG_TRACE("Processing waiting Interest list, size: " << m_waitingForProcessing.size());
180 if (m_waitingForProcessing.size() == 0) {
181 return;
182 }
183
184 for (auto it = m_waitingForProcessing.begin(); it != m_waitingForProcessing.end();) {
185 if (it->second.numTries == std::numeric_limits<uint16_t>::max()) {
186 NDN_LOG_TRACE("Interest with hash already marked for deletion, removing now: " <<
187 std::hash<ndn::Name>{}(it->first));
188 it = m_waitingForProcessing.erase(it);
189 continue;
190 }
191
192 it->second.numTries += 1;
193 ndn::Interest interest(it->first);
194 interest.setNonce(it->second.nonce);
195 onSyncInterest(m_syncPrefix, interest, true);
196 if (it->second.numTries == std::numeric_limits<uint16_t>::max()) {
197 NDN_LOG_TRACE("Removing Interest with hash: " << std::hash<ndn::Name>{}(it->first));
198 it = m_waitingForProcessing.erase(it);
199 }
200 else {
201 ++it;
202 }
203 }
204 NDN_LOG_TRACE("Done processing waiting Interest list, size: " << m_waitingForProcessing.size());
205}
206
207void
208FullProducer::scheduleProcessWaitingInterests()
209{
210 // If nothing waiting, no need to schedule
211 if (m_waitingForProcessing.size() == 0) {
212 return;
213 }
214
215 if (!m_interestDelayTimerId) {
216 auto after = ndn::time::milliseconds(m_jitter(m_rng));
217 NDN_LOG_TRACE("Setting a timer to processes waiting Interest(s) in: " << after);
218
219 m_interestDelayTimerId = m_scheduler.schedule(after, [=] {
220 NDN_LOG_TRACE("Timer has expired, trying to process waiting Interest(s)");
221 processWaitingInterests();
222 scheduleProcessWaitingInterests();
223 });
224 }
225}
226
227void
228FullProducer::onSyncInterest(const ndn::Name& prefixName, const ndn::Interest& interest,
229 bool isTimedProcessing)
230{
231 ndn::Name interestName = interest.getName();
232 auto interestNameHash = std::hash<ndn::Name>{}(interestName);
233 NDN_LOG_DEBUG("Full sync Interest received, nonce: " << interest.getNonce() <<
234 ", hash: " << interestNameHash);
235
236 if (isTimedProcessing) {
237 NDN_LOG_TRACE("Delayed Interest being processed now");
238 }
239
240 if (m_segmentPublisher.replyFromStore(interestName)) {
241 NDN_LOG_DEBUG("Answer from memory");
242 return;
243 }
244
245 ndn::Name nameWithoutSyncPrefix = interestName.getSubName(prefixName.size());
246
247 if (nameWithoutSyncPrefix.size() == 4) {
248 // /<IBF>/<numCumulativeElements>/<version>/<segment>
249 NDN_LOG_DEBUG("Segment not found in memory. Other side will have to restart");
250 // This should have been answered from publisher Cache!
251 sendApplicationNack(prefixName);
252 return;
253 }
254
255 if (nameWithoutSyncPrefix.size() != 2) {
256 NDN_LOG_WARN("Two components required after sync prefix: /<IBF>/<numCumulativeElements>; received: " << interestName);
257 return;
258 }
259
260 ndn::name::Component ibltName = interestName[-2];
261 uint64_t numRcvdElements = interestName[-1].toNumber();
262
263 detail::IBLT iblt(m_expectedNumEntries, m_ibltCompression);
264 try {
265 iblt.initialize(ibltName);
266 }
267 catch (const std::exception& e) {
268 NDN_LOG_WARN(e.what());
269 return;
270 }
271
272 auto diff = m_iblt - iblt;
273
274 NDN_LOG_TRACE("Decode, positive: " << diff.positive.size()
275 << " negative: " << diff.negative.size() << " m_threshold: "
276 << m_threshold);
277
278 auto waitingIt = m_waitingForProcessing.find(interestName);
279
280 if (!diff.canDecode) {
281 NDN_LOG_DEBUG("Cannot decode differences!");
282
283 if (numRcvdElements > m_numOwnElements) {
284 if (!isTimedProcessing && waitingIt == m_waitingForProcessing.end()) {
285 NDN_LOG_TRACE("Decode failure, adding to waiting Interest list " << interestNameHash);
286 m_waitingForProcessing.emplace(interestName, WaitingEntryInfo{0, interest.getNonce()});
287 scheduleProcessWaitingInterests();
288 }
289 else if (isTimedProcessing && waitingIt != m_waitingForProcessing.end()) {
290 if (waitingIt->second.numTries > 1) {
291 NDN_LOG_TRACE("Decode failure, still behind. Erasing waiting Interest as we have tried twice");
292 waitingIt->second.numTries = std::numeric_limits<uint16_t>::max(); // markWaitingInterestForDeletion
293 NDN_LOG_DEBUG("Waiting Interest has been deleted. Sending new sync interest");
294 sendSyncInterest();
295 }
296 else {
297 NDN_LOG_TRACE("Decode failure, still behind, waiting more till the next timer");
298 }
299 }
300 else {
301 NDN_LOG_TRACE("Decode failure, still behind");
302 }
303 }
304 else {
305 if (m_numOwnElements == numRcvdElements && diff.positive.size() == 0 && diff.negative.size() > 0) {
306 NDN_LOG_TRACE("We have nothing to offer and are actually behind");
307#ifdef PSYNC_WITH_TESTS
308 ++nIbfDecodeFailuresBelowThreshold;
309#endif // PSYNC_WITH_TESTS
310 return;
311 }
312
313 detail::State state;
314 for (const auto& content : m_prefixes) {
315 if (content.second != 0) {
316 state.addContent(ndn::Name(content.first).appendNumber(content.second));
317 }
318 }
319#ifdef PSYNC_WITH_TESTS
320 ++nIbfDecodeFailuresAboveThreshold;
321#endif // PSYNC_WITH_TESTS
322
323 if (!state.getContent().empty()) {
324 NDN_LOG_DEBUG("Sending entire state: " << state);
325 // Want low freshness when potentially sending large content to clear it quickly from the network
326 sendSyncData(interestName, state.wireEncode(), 10_ms);
327 // Since we're directly sending the data, we need to clear pending interests here
328 deletePendingInterests(interestName);
329 }
330 // We seem to be ahead, delete the Interest from waiting list
331 if (waitingIt != m_waitingForProcessing.end()) {
332 waitingIt->second.numTries = std::numeric_limits<uint16_t>::max();
333 }
334 }
335 return;
336 }
337
338 if (diff.positive.size() == 0 && diff.negative.size() == 0) {
339 NDN_LOG_TRACE("Saving positive: " << diff.positive.size() << " negative: " << diff.negative.size());
340
341 auto& entry = m_pendingEntries.emplace(interestName, PendingEntryInfo{iblt, {}}).first->second;
342 entry.expirationEvent = m_scheduler.schedule(interest.getInterestLifetime(),
343 [this, interest] {
344 NDN_LOG_TRACE("Erase pending Interest " << interest.getNonce());
345 m_pendingEntries.erase(interest.getName());
346 });
347
348 // Can't delete directly in this case as it will cause
349 // memory access errors with the for loop in processWaitingInterests
350 if (isTimedProcessing) {
351 if (waitingIt != m_waitingForProcessing.end()) {
352 waitingIt->second.numTries = std::numeric_limits<uint16_t>::max();
353 }
354 }
355 return;
356 }
357
358 // Only add to waiting list if we don't have anything to send (positive = 0)
359 if (diff.positive.size() == 0 && diff.negative.size() > 0) {
360 if (!isTimedProcessing && waitingIt == m_waitingForProcessing.end()) {
361 NDN_LOG_TRACE("Adding Interest to waiting list: " << interestNameHash);
362 m_waitingForProcessing.emplace(interestName, WaitingEntryInfo{0, interest.getNonce()});
363 scheduleProcessWaitingInterests();
364 }
365 else if (isTimedProcessing && waitingIt != m_waitingForProcessing.end()) {
366 if (waitingIt->second.numTries > 1) {
367 NDN_LOG_TRACE("Still behind after waiting for Interest " << interestNameHash <<
368 ". Erasing waiting Interest as we have tried twice");
369 waitingIt->second.numTries = std::numeric_limits<uint16_t>::max(); // markWaitingInterestForDeletion
370 }
371 else {
372 NDN_LOG_TRACE("Still behind after waiting for Interest " << interestNameHash <<
373 ". Keep waiting for Interest as number of tries is not exhausted");
374 }
375 }
376 else {
377 NDN_LOG_TRACE("Still behind after waiting for Interest " << interestNameHash);
378 }
379 return;
380 }
381
382 if (diff.positive.size() > 0) {
383 detail::State state;
384 for (const auto& hash : diff.positive) {
385 auto nameIt = m_biMap.left.find(hash);
386 if (nameIt != m_biMap.left.end()) {
387 ndn::Name nameWithoutSeq = nameIt->second.getPrefix(-1);
388 // Don't sync up sequence number zero
389 if (m_prefixes[nameWithoutSeq] != 0 &&
390 !isFutureHash(nameWithoutSeq.toUri(), diff.negative)) {
391 state.addContent(nameIt->second);
392 }
393 }
394 }
395
396 if (!state.getContent().empty()) {
397 NDN_LOG_DEBUG("Sending sync content: " << state);
398 sendSyncData(interestName, state.wireEncode(), m_syncReplyFreshness);
399
400 // Timed processing or not - if we are answering it, it should not go in waiting Interests
401 if (waitingIt != m_waitingForProcessing.end()) {
402 waitingIt->second.numTries = std::numeric_limits<uint16_t>::max();
403 }
404 }
405 }
406}
407
408void
409FullProducer::sendSyncData(const ndn::Name& name, const ndn::Block& block,
410 ndn::time::milliseconds syncReplyFreshness)
411{
412 bool isSatisfyingOwnInterest = m_outstandingInterestName == name;
413 if (isSatisfyingOwnInterest && m_fetcher) {
414 NDN_LOG_DEBUG("Removing our pending Interest from face (stop fetcher)");
415 m_fetcher->stop();
416 m_outstandingInterestName.clear();
417 }
418
419 NDN_LOG_DEBUG("Sending sync Data");
420 auto content = detail::compress(m_contentCompression, block);
421 m_segmentPublisher.publish(name, name, *content, syncReplyFreshness);
422 if (isSatisfyingOwnInterest) {
423 NDN_LOG_DEBUG("Renewing sync interest");
424 sendSyncInterest();
425 }
426}
427
428void
429FullProducer::onSyncData(const ndn::Interest& interest, const ndn::ConstBufferPtr& bufferPtr)
430{
431 deletePendingInterests(interest.getName());
432
433 detail::State state;
434 try {
435 auto decompressed = detail::decompress(m_contentCompression, *bufferPtr);
436 state.wireDecode(ndn::Block(std::move(decompressed)));
437 }
438 catch (const std::exception& e) {
439 NDN_LOG_ERROR("Cannot parse received sync Data: " << e.what());
440 return;
441 }
442 NDN_LOG_DEBUG("Sync Data received: " << state);
443
444 std::vector<MissingDataInfo> updates;
445
446 for (const auto& content : state) {
447 ndn::Name prefix = content.getPrefix(-1);
448 uint64_t seq = content.get(content.size() - 1).toNumber();
449
450 if (m_prefixes.find(prefix) == m_prefixes.end() || m_prefixes[prefix] < seq) {
451 updates.push_back({prefix, m_prefixes[prefix] + 1, seq, m_incomingFace});
452 updateSeqNo(prefix, seq);
453 // We should not call satisfyPendingSyncInterests here because we just
454 // got data and deleted pending interest by calling deletePendingFullSyncInterests
455 // But we might have interests not matching to this interest that might not have deleted
456 // from pending sync interest
457 }
458 }
459
460 if (!updates.empty()) {
461 m_onUpdate(updates);
462 // Wait a bit to let neighbors get the data too
463 auto after = ndn::time::milliseconds(m_jitter(m_rng));
464 m_scheduledSyncInterestId = m_scheduler.schedule(after, [this] {
465 NDN_LOG_DEBUG("Got updates, renewing sync Interest now");
466 sendSyncInterest();
467 });
468 NDN_LOG_DEBUG("Schedule sync Interest after: " << after);
469 m_inNoNewDataWaitOutPeriod = false;
470
471 processWaitingInterests();
472 }
473 else {
474 NDN_LOG_TRACE("No new update, Interest nonce: " << interest.getNonce() <<
475 " , hash: " << std::hash<ndn::Name>{}(interest.getName()));
476 m_inNoNewDataWaitOutPeriod = true;
477
478 // Have to wait, otherwise will get same data from CS
479 auto after = m_syncReplyFreshness + ndn::time::milliseconds(m_jitter(m_rng));
480 m_scheduledSyncInterestId = m_scheduler.schedule(after, [this] {
481 NDN_LOG_DEBUG("Sending sync Interest after no new update");
482 m_inNoNewDataWaitOutPeriod = false;
483 sendSyncInterest();
484 });
485 NDN_LOG_DEBUG("Schedule sync after: " << after);
486 }
487
488}
489
490void
491FullProducer::satisfyPendingInterests(const ndn::Name& updatedPrefixWithSeq)
492{
493 NDN_LOG_DEBUG("Satisfying full sync Interest: " << m_pendingEntries.size());
494
495 for (auto it = m_pendingEntries.begin(); it != m_pendingEntries.end();) {
496 NDN_LOG_TRACE("Satisfying pending Interest: " << std::hash<ndn::Name>{}(it->first.getPrefix(-1)));
497 const auto& entry = it->second;
498 auto diff = m_iblt - entry.iblt;
499 NDN_LOG_TRACE("Decoded: " << diff.canDecode << " positive: " << diff.positive.size() <<
500 " negative: " << diff.negative.size());
501
502 detail::State state;
503 bool publishedPrefixInDiff = false;
504 for (const auto& hash : diff.positive) {
505 auto nameIt = m_biMap.left.find(hash);
506 if (nameIt != m_biMap.left.end()) {
507 if (updatedPrefixWithSeq == nameIt->second) {
508 publishedPrefixInDiff = true;
509 }
510 state.addContent(nameIt->second);
511 }
512 }
513
514 if (!publishedPrefixInDiff) {
515 state.addContent(updatedPrefixWithSeq);
516 }
517
518 NDN_LOG_DEBUG("Satisfying sync content: " << state);
519 sendSyncData(it->first, state.wireEncode(), m_syncReplyFreshness);
520 it = m_pendingEntries.erase(it);
521 }
522}
523
524bool
525FullProducer::isFutureHash(const ndn::Name& prefix, const std::set<uint32_t>& negative)
526{
528 ndn::Name(prefix).appendNumber(m_prefixes[prefix] + 1));
529 return negative.find(nextHash) != negative.end();
530}
531
532void
533FullProducer::deletePendingInterests(const ndn::Name& interestName)
534{
535 auto it = m_pendingEntries.find(interestName);
536 if (it != m_pendingEntries.end()) {
537 NDN_LOG_TRACE("Delete pending Interest: " << std::hash<ndn::Name>{}(interestName));
538 it = m_pendingEntries.erase(it);
539 }
540}
541
542} // namespace psync
Full sync logic to synchronize with other nodes where all nodes wants to get all names prefixes synce...
void publishName(const ndn::Name &prefix, std::optional< uint64_t > seq=std::nullopt)
Publish name to let others know.
FullProducer(ndn::Face &face, ndn::KeyChain &keyChain, const ndn::Name &syncPrefix, const Options &opts)
Constructor.
Base class for PartialProducer and FullProducer.
const CompressionScheme m_contentCompression
void sendApplicationNack(const ndn::Name &name)
Sends a data packet with content type nack.
const ndn::Name m_syncPrefix
const ndn::time::milliseconds m_syncReplyFreshness
const size_t m_expectedNumEntries
bool addUserNode(const ndn::Name &prefix)
Adds a user node for synchronization.
std::map< ndn::Name, uint64_t > m_prefixes
ndn::Scheduler m_scheduler
static void onRegisterFailed(const ndn::Name &prefix, const std::string &msg)
Logs a message and throws if setting an interest filter fails.
const CompressionScheme m_ibltCompression
void updateSeqNo(const ndn::Name &prefix, uint64_t seq)
Update m_prefixes and IBF with the given prefix and seq.
SegmentPublisher m_segmentPublisher
ndn::random::RandomNumberEngine & m_rng
bool replyFromStore(const ndn::Name &interestName)
Try to reply from memory, return false if we cannot find the segment.
void publish(const ndn::Name &interestName, const ndn::Name &dataName, ndn::span< const uint8_t > buffer, ndn::time::milliseconds freshness)
Put all the segments in memory.
void appendToName(ndn::Name &name) const
Appends self to name.
Definition iblt.cpp:138
uint32_t murmurHash3(const void *key, size_t len, uint32_t seed)
Definition util.cpp:58
constexpr size_t N_HASHCHECK
Definition iblt.hpp:87
std::shared_ptr< ndn::Buffer > compress(CompressionScheme scheme, ndn::span< const uint8_t > buffer)
Definition util.cpp:124
std::shared_ptr< ndn::Buffer > decompress(CompressionScheme scheme, ndn::span< const uint8_t > buffer)
Definition util.cpp:183
CompressionScheme
Definition common.hpp:43
std::function< void(const std::vector< MissingDataInfo > &)> UpdateCallback
Definition common.hpp:71