folly/folly/observer/HazptrObserver.h

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <atomic>
#include <memory>

#include <folly/Synchronized.h>
#include <folly/experimental/observer/detail/ObserverManager.h>
#include <folly/observer/Observer.h>
#include <folly/synchronization/Hazptr.h>

namespace folly {
namespace observer {

/**
 * HazptrObserver implements a read-optimized Observer which caches an
 * Observer's snapshot and protects access to it using hazptrs. The cached
 * snapshot is kept up to date using a callback which fires when the original
 * observer changes. This implementation incurs an additional allocation
 * on updates making it less suitable for write-heavy workloads.
 *
 * There are 2 main APIs:
 * 1) getSnapshot: Returns a Snapshot containing a const pointer to T and guards
 *    access to it using folly::hazptr_holder. The pointer is only safe to use
 *    while the returned Snapshot object is alive.
 * 2) getLocalSnapshot: Same as getSnapshot but backed by folly::hazptr_local.
 *    This API is ~3ns faster than getSnapshot but is unsafe for the current
 *    thread to construct any other hazptr holder type objects (hazptr_holder,
 *    hazptr_array and other hazptr_local) while the returned snapshot exists.
 *
 * See folly/synchronization/Hazptr.h for more details on hazptrs.
 */
template <typename T, template <typename> class Atom = std::atomic>
class HazptrObserver {
  template <typename Holder>
  struct HazptrSnapshot {
    template <typename State>
    explicit HazptrSnapshot(
        const Atom<State*>& state, hazptr_domain<Atom>& domain)
        : holder_() {
      make(holder_, domain);
      ptr_ = get(holder_).protect(state)->snapshot_.get();
    }

    const T& operator*() const { return *get(); }
    const T* operator->() const { return get(); }
    const T* get() const { return ptr_; }

   private:
    static void make(hazptr_holder<Atom>& holder, hazptr_domain<Atom>& domain) {
      holder = folly::make_hazard_pointer(domain);
    }
    static void make(hazptr_local<1, Atom>&, hazptr_domain<Atom>&) {}
    static hazptr_holder<Atom>& get(hazptr_holder<Atom>& holder) {
      return holder;
    }
    static hazptr_holder<Atom>& get(hazptr_local<1>& holder) {
      return holder[0];
    }

    Holder holder_;
    const T* ptr_;
  };

 public:
  using DefaultSnapshot = HazptrSnapshot<hazptr_holder<Atom>>;
  using LocalSnapshot = HazptrSnapshot<hazptr_local<1>>;

  explicit HazptrObserver(
      Observer<T> observer,
      hazptr_domain<Atom>& domain = default_hazptr_domain<Atom>())
      : domain_{domain},
        observer_(observer),
        updateObserver_(
            makeObserver([o = std::move(observer), alive = alive_, this]() {
              auto snapshot = o.getSnapshot();
              auto oldState = static_cast<State*>(nullptr);
              alive->withRLock([&](auto vAlive) {
                if (vAlive) { // otherwise state_ may be out-of-scope
                  oldState = state_.exchange(
                      new State(snapshot), std::memory_order_acq_rel);
                }
              });
              if (oldState) {
                oldState->retire(domain_);
              }
              return folly::unit;
            })) {}

  // updateObserver_ captures this, so we cannot move it, hence only the copy
  // constructor is defined (moves will fall back to copy).
  HazptrObserver(const HazptrObserver& r)
      : HazptrObserver(r.observer_, r.domain_) {}
  HazptrObserver& operator=(const HazptrObserver& r) {
    if (&r != this) {
      this->~HazptrObserver();
      new (this) HazptrObserver(r);
    }
    return *this;
  }

  ~HazptrObserver() {
    *alive_->wlock() = false;
    auto* state = state_.load(std::memory_order_acquire);
    if (state) {
      state->retire(domain_);
    }
  }

  DefaultSnapshot getSnapshot() const {
    if (FOLLY_UNLIKELY(observer_detail::ObserverManager::inManagerThread())) {
      // Wait for updates
      updateObserver_.getSnapshot();
    }
    return DefaultSnapshot(state_, domain_);
  }

  LocalSnapshot getLocalSnapshot() const {
    if (FOLLY_UNLIKELY(observer_detail::ObserverManager::inManagerThread())) {
      // Wait for updates
      updateObserver_.getSnapshot();
    }
    return LocalSnapshot(state_, domain_);
  }

 private:
  struct State : public hazptr_obj_base<State, Atom> {
    explicit State(Snapshot<T> snapshot) : snapshot_(std::move(snapshot)) {}

    Snapshot<T> snapshot_;
  };

  Atom<State*> state_{nullptr};
  std::shared_ptr<Synchronized<bool>> alive_{
      std::make_shared<Synchronized<bool>>(true)};
  hazptr_domain<Atom>& domain_;
  Observer<T> observer_;
  Observer<folly::Unit> updateObserver_;
};

/**
 * Same as makeObserver(...), but creates HazptrObserver.
 */
template <typename T>
HazptrObserver<T> makeHazptrObserver(Observer<T> observer) {
  return HazptrObserver<T>(std::move(observer));
}

template <typename F>
auto makeHazptrObserver(F&& creator) {
  return makeHazptrObserver(makeObserver(std::forward<F>(creator)));
}

} // namespace observer
} // namespace folly