// Copyright (C) 2023 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef TRACKING_LEASE_MGR_H
#define TRACKING_LEASE_MGR_H

#include <asiolink/io_address.h>
#include <dhcpsrv/lease_mgr.h>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/composite_key.hpp>
#include <boost/multi_index/indexed_by.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/shared_ptr.hpp>
#include <functional>
#include <string>
#include <unordered_set>

namespace isc {
namespace dhcp {

/// @brief Introduces callbacks into the @c LeaseMgr.
///
/// The LeaseMgr is a central point of lease management and is aware of all
/// lease changes within the server instance. Thus, it is a good opportunity
/// to allow installing callbacks in the @c LeaseMgr to track all lease
/// changes. An allocator maintaining a list of free leases (FLQ allocator)
/// can benefit from it by/ installing the callbacks that add or remove
/// free leases from this list, depending on the lease manager's activity.
/// The callbacks are invoked regardless if the lease changes result from a
/// normal lease allocation or a control command. The callbacks can also be
/// useful for maintaining a log of lease changes. Such a log could be made
/// available externally and consumed by another application (e.g., Stork).
///
/// The lease manager can track three types of calls: new lease insertion
/// (add), an existing lease update, and lease deletion. Even though the
/// lease reclamation is similar to deleting a lease because it becomes free,
/// it is a lease update from the lease manager's standpoint. Currently,
/// the lease manager cannot track the deletion of the reclaimed leases
/// (i.e., the leases in the expired-reclaimed state).
///
/// The lease backends should call the suitable tracking functions:
/// @c trackAddLease, @c trackUpdateLease, and @c trackDeleteLease.
/// However, the backends must ensure that there are no race conditions
/// between modifying the lease in the database and running the callbacks.
/// Suppose that two threads modify the same lease. Thread A inserts the
/// lease in the database, and thread B removes it. The callback for
/// thread A should be invoked before the callback for thread B. If they
/// are invoked in reverse order, it can result in an inconsistent state
/// in the free lease queue allocator because the allocator should record
/// the lease as available after the thread B callback. The reverse
/// invocation order would result in marking the lease as unavailable for
/// allocation after both callbacks.
///
/// The race condition does not occur for the Memfile backend because it
/// guards entire functions with a mutex. However, the SQL backends rely
/// on the database to guard against concurrent writes. In these cases,
/// the backend must protect against the reverse order of callbacks. They
/// should use the lease locking mechanism introduced in the
/// @c TrackingLeaseMgr.
///
/// The lease locking is modeled on an @c unordered_set container holding
/// the leases with the ongoing allocations. The leases are inserted into
/// this container by the @c tryLock function. If another thread has already
/// locked the lease, this function returns @c false to indicate an
/// unsuccessful attempt. In this case, the thread should resign from updating
/// the lease and return early. It can result in a lease allocation failure,
/// but two concurrent threads extremely rarely work on allocating a lease for
/// the same client. A passive wait could be another option here, but it is a
/// much more complicated solution for a bit of gain.
class TrackingLeaseMgr : public LeaseMgr {
public:

    /// The @c LeaseMgrFactory manages the @c LeaseMgr instances and has
    /// to be able to move installed callbacks between them. No other external
    /// class can have access to the callbacks container. Thus, we can't make
    /// the container public. The friend declaration deals with it cleanly.
    friend class LeaseMgrFactory;

    /// @brief An enumeration differentiating between lease write operations.
    typedef enum {
        TRACK_ADD_LEASE,
        TRACK_UPDATE_LEASE,
        TRACK_DELETE_LEASE
    } CallbackType;

    /// @brief Type of a callback function invoked upon a lease insertion,
    /// update or deletion.
    ///
    /// The first argument is a pointer to the lease for which the callback
    /// is invoked. The second argument indicates whether or not the callback
    /// is invoked in the thread-safe context.
    typedef std::function<void(LeasePtr, bool)> CallbackFn;

    /// @brief A structure representing a registered callback.
    ///
    /// It associates the callback with a type, its owner, subnet
    /// identifier, and a lease type. The owner is a string specified
    /// by the registration function caller. There must be at most one
    /// callback registered for the particular owner, subnet identifier
    /// and the lease type.
    typedef struct {
        /// Callback type (i.e., lease add, update, delete).
        CallbackType type;
        /// An entity owning callback registration (e.g., FLQ allocator).
        std::string owner;
        /// Subnet identifier associated with the callback.
        SubnetID subnet_id;
        /// Lease types for which the callback should be invoked.
        Lease::Type lease_type;
        /// Callback function.
        CallbackFn fn;
    } Callback;

protected:

    /// @brief A multi-index container holding registered callbacks.
    ///
    /// The callbacks are accessible via two indexes. The first composite index
    /// filters the callbacks by the callback type (i.e., lease add, update or delete)
    /// and the subnet id. The second index filters the callbacks by the subnet id
    /// and the lease type.
    typedef boost::multi_index_container<
        Callback,
        boost::multi_index::indexed_by<
            boost::multi_index::ordered_non_unique<
                boost::multi_index::composite_key<
                    Callback,
                    boost::multi_index::member<Callback, CallbackType, &Callback::type>,
                    boost::multi_index::member<Callback, SubnetID, &Callback::subnet_id>,
                    boost::multi_index::member<Callback, Lease::Type, &Callback::lease_type>
                >
            >,
            boost::multi_index::ordered_non_unique<
                boost::multi_index::composite_key<
                    Callback,
                    boost::multi_index::member<Callback, SubnetID, &Callback::subnet_id>,
                    boost::multi_index::member<Callback, Lease::Type, &Callback::lease_type>
                >
            >
        >
    > CallbackContainer;

    /// @brief Pointer to the callback container.
    typedef boost::shared_ptr<CallbackContainer> CallbackContainerPtr;

    /// @brief Constructor.
    TrackingLeaseMgr();

    /// @brief Attempts to lock a lease.
    ///
    /// If a lease is successfully locked, no other thread can lock it. It protects
    /// against running the callbacks out of order when two threads modify the same
    /// lease. Such a locking should only be used when the lease allocation followed by
    /// the callbacks invocation are not protected by some other synchronization
    /// mechanism. In particular, the Memfile backend uses a mutex for locking in the
    /// lease allocation functions. In this case, it is unnecessary to apply a lock at the
    /// lease level. The SQL backends rely on the database locking mechanisms to prevent
    /// the concurrent updates of the same lease. These backends must use the lease locking
    /// to ensure the correct callbacks invocation order.
    ///
    /// This function is not thread-safe and must be invoked in a thread-safe context.
    ///
    /// @param lease a lease instance for which the lock should be attempted.
    /// @return true when locking was successful, false otherwise. In the latter case,
    /// the thread should stop a lease allocation or deallocation attempt.
    bool tryLock(const LeasePtr& lease);

    /// @brief Attempts to unlock a lease.
    ///
    /// This function is not thread-safe and must be invoked in a thread-safe context.
    ///
    /// @param lease a lease instance for which unlocking should be attempted.
    void unlock(const LeasePtr& lease);

public:

    /// @brief Checks if the lease is locked.
    ///
    /// This function is useful in the unit tests.
    ///
    /// @return true if the lease is locked, false otherwise.
    bool isLocked(const LeasePtr& lease);

protected:

    /// @brief Invokes the callbacks when a new lease is added.
    ///
    /// It executes all callbacks of the @c TRACK_ADD_LEASE type for a subnet id of 0
    /// and the subnet id associated with the lease.
    ///
    /// The callbacks execution order is not guaranteed.
    ///
    /// @param lease new lease instance.
    /// @param mt_safe a boolean flag indicating whether the callbacks are
    /// invoked in the MT-safe context.
    void trackAddLease(const LeasePtr& lease, bool mt_safe);

    /// @brief Invokes the callbacks when a lease is updated.
    ///
    /// It executes all callbacks of the @c TRACK_UPDATE_LEASE type for a subnet id of 0
    /// and the subnet id associated with the lease.
    ///
    /// The callbacks execution order is not guaranteed.
    ///
    /// @param lease updated lease instance.
    /// @param mt_safe a boolean flag indicating whether the callbacks are
    /// invoked in the MT-safe context.
    void trackUpdateLease(const LeasePtr& lease, bool mt_safe);

    /// @brief Invokes the callbacks when a lease is deleted.
    ///
    /// It executes all callbacks of the @c TRACK_DELETE_LEASE type for a subnet id of 0
    /// and the subnet id associated with the lease.
    ///
    /// The callbacks execution order is not guaranteed.
    ///
    /// @param lease deleted lease instance.
    /// @param mt_safe a boolean flag indicating whether the callbacks are
    /// invoked in the MT-safe context.
    void trackDeleteLease(const LeasePtr& lease, bool mt_safe);

public:

    /// @brief Registers a callback function for a subnet.
    ///
    /// @param type callback type.
    /// @param owner callback owner identifier.
    /// @param subnet_id subnet identifier; it can be set to 0 if the callback should be
    /// called for all subnets.
    /// @param lease_type a lease type.
    /// @param callback_fn callback function instance.
    /// @throw InvalidOperation when the callback has been already registered for the given owner and
    /// the subnet identifier.
    void registerCallback(CallbackType type, std::string owner, SubnetID subnet_id,
                          Lease::Type lease_type, CallbackFn callback_fn);

    /// @brief Registers a callback function for all subnets.
    ///
    /// @param type callback type.
    /// @param owner callback owner identifier.
    /// @param lease_type a lease type.
    /// @param callback_fn callback function instance.
    /// @throw InvalidOperation when the callback has been already registered for the given owner and
    /// all subnets.
    void registerCallback(CallbackType type, std::string owner, Lease::Type lease_type,
                          CallbackFn callback_fn);

    /// @brief Unregisters all callbacks for a given subnet identifier.
    ///
    /// @param subnet_id a subnet identifier.
    /// @param lease_type a lease type.
    void unregisterCallbacks(SubnetID subnet_id, Lease::Type lease_type);

    /// @brief Unregisters all callbacks.
    void unregisterAllCallbacks();

    /// @brief Checks if any callbacks have been registered.
    ///
    /// It is a quick check to be performed by the backends whether or not
    /// the callbacks mechanism is used.
    ///
    /// @return true if any callbacks have been registered.
    bool hasCallbacks() const;

protected:

    /// @brief Converts callback type to string for logging purposes.
    ///
    /// @param type callback type.
    /// @return callback type name or the 'unknown' string.
    static std::string callbackTypeToString(CallbackType type);

    /// @brief Runs registered callbacks of the particular type.
    ///
    /// The specified lease instance contains the subnet identifier used to
    /// filter the callbacks to be invoked.
    ///
    /// @param type callback type.
    /// @param lease lease instance for which the callbacks are invoked.
    /// @param mt_safe a boolean flag indicating whether the callbacks are
    /// invoked in the MT-safe context.
    void runCallbacks(CallbackType type, const LeasePtr& lease, bool mt_safe);

    /// @brief Runs registered callbacks of the particular type for a subnet id.
    ///
    /// It is called internally by the @c runCallbacks function.
    ///
    /// @param type callback type.
    /// @param subnet_id subnet identifier for which the callbacks are invoked.
    /// @param lease lease instance for which the callbacks are invoked.
    /// @param mt_safe a boolean flag indicating whether the callbacks are
    /// invoked in the MT-safe context.
    void runCallbacksForSubnetID(CallbackType type, SubnetID subnet_id,
                                 const LeasePtr& lease, bool mt_safe);

    /// @brief The multi-index container holding registered callbacks.
    CallbackContainerPtr callbacks_;

    /// @brief A set of locked leases.
    ///
    /// It is empty if locking is not used (e.g. Memfile backend) or when there
    /// are no ongoing allocations.
    std::unordered_set<asiolink::IOAddress, asiolink::IOAddress::Hash> locked_leases_;
};

} // end of namespace isc::dhcp
} // end of namespace isc

#endif // TRACKING_LEASE_MGR_H
