Blob Blame History Raw
/*
 * Copyright (c) 2020 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA. 
 *
 * $Id: //eng/vdo-releases/aluminum/src/c++/vdo/base/hashZone.c#3 $
 */

#include "hashZone.h"

#include "logger.h"
#include "memoryAlloc.h"
#include "numeric.h"
#include "permassert.h"

#include "constants.h"
#include "dataVIO.h"
#include "hashLock.h"
#include "hashLockInternals.h"
#include "pointerMap.h"
#include "ringNode.h"
#include "statistics.h"
#include "threadConfig.h"
#include "types.h"
#include "vdoInternal.h"

enum {
  LOCK_POOL_CAPACITY = MAXIMUM_USER_VIOS,
};

/**
 * These fields are only modified by the locks sharing the hash zone thread,
 * but are queried by other threads.
 **/
typedef struct atomicHashLockStatistics {
  /** Number of times the UDS advice proved correct */
  Atomic64 dedupeAdviceValid;

  /** Number of times the UDS advice proved incorrect */
  Atomic64 dedupeAdviceStale;

  /** Number of writes with the same data as another in-flight write */
  Atomic64 concurrentDataMatches;

  /** Number of writes whose hash collided with an in-flight write */
  Atomic64 concurrentHashCollisions;
} AtomicHashLockStatistics;

struct hashZone {
  /** Which hash zone this is */
  ZoneCount zoneNumber;

  /** The thread ID for this zone */
  ThreadID threadID;

  /** Mapping from chunkName fields to HashLocks */
  PointerMap *hashLockMap;

  /** Ring containing all unused HashLocks */
  RingNode lockPool;

  /** Statistics shared by all hash locks in this zone */
  AtomicHashLockStatistics statistics;

  /** Array of all HashLocks */
  HashLock *lockArray;
};

/**
 * Implements PointerKeyComparator.
 **/
static bool compareKeys(const void *thisKey, const void *thatKey)
{
  // Null keys are not supported.
  return (memcmp(thisKey, thatKey, sizeof(UdsChunkName)) == 0);
}

/**
 * Implements PointerKeyComparator.
 **/
static uint32_t hashKey(const void *key)
{
  const UdsChunkName *name = key;
  /*
   * Use a fragment of the chunk name as a hash code. It must not overlap with
   * fragments used elsewhere to ensure uniform distributions.
   */
  // XXX pick an offset in the chunk name that isn't used elsewhere
  return getUInt32LE(&name->name[4]);
}

/**********************************************************************/
static inline HashLock *asHashLock(RingNode *poolNode)
{
  STATIC_ASSERT(offsetof(HashLock, poolNode) == 0);
  return (HashLock *) poolNode;
}

/**********************************************************************/
int makeHashZone(VDO *vdo, ZoneCount zoneNumber, HashZone **zonePtr)
{
  HashZone *zone;
  int result = ALLOCATE(1, HashZone, __func__, &zone);
  if (result != VDO_SUCCESS) {
    return result;
  }

  result = makePointerMap(LOCK_MAP_CAPACITY, 0, compareKeys, hashKey,
                          &zone->hashLockMap);
  if (result != VDO_SUCCESS) {
    freeHashZone(&zone);
    return result;
  }

  zone->zoneNumber = zoneNumber;
  zone->threadID   = getHashZoneThread(getThreadConfig(vdo), zoneNumber);
  initializeRing(&zone->lockPool);

  result = ALLOCATE(LOCK_POOL_CAPACITY, HashLock, "HashLock array",
                    &zone->lockArray);
  if (result != VDO_SUCCESS) {
    freeHashZone(&zone);
    return result;
  }

  for (VIOCount i = 0; i < LOCK_POOL_CAPACITY; i++) {
    HashLock *lock = &zone->lockArray[i];
    initializeHashLock(lock);
    pushRingNode(&zone->lockPool, &lock->poolNode);
  }

  *zonePtr = zone;
  return VDO_SUCCESS;
}

/**********************************************************************/
void freeHashZone(HashZone **zonePtr)
{
  if (*zonePtr == NULL) {
    return;
  }

  HashZone *zone = *zonePtr;
  freePointerMap(&zone->hashLockMap);
  FREE(zone->lockArray);
  FREE(zone);
  *zonePtr = NULL;
}

/**********************************************************************/
ZoneCount getHashZoneNumber(const HashZone *zone)
{
  return zone->zoneNumber;
}

/**********************************************************************/
ThreadID getHashZoneThreadID(const HashZone *zone)
{
  return zone->threadID;
}

/**********************************************************************/
HashLockStatistics getHashZoneStatistics(const HashZone *zone)
{
  const AtomicHashLockStatistics *atoms = &zone->statistics;
  return (HashLockStatistics) {
    .dedupeAdviceValid     = relaxedLoad64(&atoms->dedupeAdviceValid),
    .dedupeAdviceStale     = relaxedLoad64(&atoms->dedupeAdviceStale),
    .concurrentDataMatches = relaxedLoad64(&atoms->concurrentDataMatches),
    .concurrentHashCollisions
      = relaxedLoad64(&atoms->concurrentHashCollisions),
  };
}

/**
 * Return a hash lock to the zone's pool and null out the reference to it.
 *
 * @param [in]     zone     The zone from which the lock was borrowed
 * @param [in,out] lockPtr  The last reference to the lock being returned
 **/
static void returnHashLockToPool(HashZone *zone, HashLock **lockPtr)
{
  HashLock *lock = *lockPtr;
  *lockPtr = NULL;

  memset(lock, 0, sizeof(*lock));
  initializeHashLock(lock);
  pushRingNode(&zone->lockPool, &lock->poolNode);
}

/**********************************************************************/
int acquireHashLockFromZone(HashZone            *zone,
                            const UdsChunkName  *hash,
                            HashLock            *replaceLock,
                            HashLock           **lockPtr)
{
  // Borrow and prepare a lock from the pool so we don't have to do two
  // PointerMap accesses in the common case of no lock contention.
  HashLock *newLock = asHashLock(popRingNode(&zone->lockPool));
  int result = ASSERT(newLock != NULL,
                      "never need to wait for a free hash lock");
  if (result != VDO_SUCCESS) {
    return result;
  }

  // Fill in the hash of the new lock so we can map it, since we have to use
  // the hash as the map key.
  newLock->hash = *hash;

  HashLock *lock;
  result = pointerMapPut(zone->hashLockMap, &newLock->hash, newLock,
                         (replaceLock != NULL), (void **) &lock);
  if (result != VDO_SUCCESS) {
    returnHashLockToPool(zone, &newLock);
    return result;
  }

  if (replaceLock != NULL) {
    // XXX on mismatch put the old lock back and return a severe error
    ASSERT_LOG_ONLY(lock == replaceLock,
                    "old lock must have been in the lock map");
    // XXX check earlier and bail out?
    ASSERT_LOG_ONLY(replaceLock->registered,
                    "old lock must have been marked registered");
    replaceLock->registered = false;
  }

  if (lock == replaceLock) {
    lock = newLock;
    lock->registered = true;
  } else {
    // There's already a lock for the hash, so we don't need the borrowed lock.
    returnHashLockToPool(zone, &newLock);
  }

  *lockPtr = lock;
  return VDO_SUCCESS;
}

/**********************************************************************/
void returnHashLockToZone(HashZone *zone, HashLock **lockPtr)
{
  HashLock *lock = *lockPtr;
  *lockPtr = NULL;

  if (lock->registered) {
    HashLock *removed = pointerMapRemove(zone->hashLockMap, &lock->hash);
    ASSERT_LOG_ONLY(lock == removed,
                    "hash lock being released must have been mapped");
  } else {
    ASSERT_LOG_ONLY(lock != pointerMapGet(zone->hashLockMap, &lock->hash),
                    "unregistered hash lock must not be in the lock map");
  }

  ASSERT_LOG_ONLY(!hasWaiters(&lock->waiters),
                  "hash lock returned to zone must have no waiters");
  ASSERT_LOG_ONLY((lock->duplicateLock == NULL),
                  "hash lock returned to zone must not reference a PBN lock");
  ASSERT_LOG_ONLY((lock->state == HASH_LOCK_DESTROYING),
                  "returned hash lock must not be in use with state %s",
                  getHashLockStateName(lock->state));
  ASSERT_LOG_ONLY(isRingEmpty(&lock->poolNode),
                  "hash lock returned to zone must not be in a pool ring");
  ASSERT_LOG_ONLY(isRingEmpty(&lock->duplicateRing),
                  "hash lock returned to zone must not reference DataVIOs");

  returnHashLockToPool(zone, &lock);
}

/**
 * Dump a compact description of HashLock to the log if the lock is not on the
 * free list.
 *
 * @param lock  The hash lock to dump
 **/
static void dumpHashLock(const HashLock *lock)
{
  if (!isRingEmpty(&lock->poolNode)) {
    // This lock is on the free list.
    return;
  }

  // Necessarily cryptic since we can log a lot of these. First three chars of
  // state is unambiguous. 'U' indicates a lock not registered in the map.
  const char *state = getHashLockStateName(lock->state);
  logInfo("  hl %" PRIptr ": %3.3s %c%llu/%u rc=%u wc=%zu agt=%" PRIptr,
          (const void *) lock,
          state,
          (lock->registered ? 'D' : 'U'),
          lock->duplicate.pbn,
          lock->duplicate.state,
          lock->referenceCount,
          countWaiters(&lock->waiters),
          (void *) lock->agent);
}

/**********************************************************************/
void bumpHashZoneValidAdviceCount(HashZone *zone)
{
  // Must only be mutated on the hash zone thread.
  relaxedAdd64(&zone->statistics.dedupeAdviceValid, 1);
}

/**********************************************************************/
void bumpHashZoneStaleAdviceCount(HashZone *zone)
{
  // Must only be mutated on the hash zone thread.
  relaxedAdd64(&zone->statistics.dedupeAdviceStale, 1);
}

/**********************************************************************/
void bumpHashZoneDataMatchCount(HashZone *zone)
{
  // Must only be mutated on the hash zone thread.
  relaxedAdd64(&zone->statistics.concurrentDataMatches, 1);
}

/**********************************************************************/
void bumpHashZoneCollisionCount(HashZone *zone)
{
  // Must only be mutated on the hash zone thread.
  relaxedAdd64(&zone->statistics.concurrentHashCollisions, 1);
}

/**********************************************************************/
void dumpHashZone(const HashZone *zone)
{
  if (zone->hashLockMap == NULL) {
    logInfo("HashZone %u: NULL map", zone->zoneNumber);
    return;
  }

  logInfo("HashZone %u: mapSize=%zu",
          zone->zoneNumber, pointerMapSize(zone->hashLockMap));
  for (VIOCount i = 0; i < LOCK_POOL_CAPACITY; i++) {
    dumpHashLock(&zone->lockArray[i]);
  }
}