]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4425] Implement SharedFlqAllocator class
authorThomas Markwalder <tmark@isc.org>
Thu, 2 Apr 2026 19:16:46 +0000 (15:16 -0400)
committerThomas Markwalder <tmark@isc.org>
Thu, 2 Apr 2026 19:16:46 +0000 (15:16 -0400)
modified:   src/lib/dhcpsrv/cfg_subnets4.cc
modified:   src/lib/dhcpsrv/cfg_subnets6.cc
modified:   src/lib/dhcpsrv/iterative_allocator.h
modified:   src/lib/dhcpsrv/meson.build
new file:   src/lib/dhcpsrv/sflq_allocator.cc
new file:   src/lib/dhcpsrv/sflq_allocator.h
modified:   src/lib/dhcpsrv/tests/meson.build
new file:   src/lib/dhcpsrv/tests/sflq_allocator_unittest.cc
modified:   src/lib/dhcpsrv/testutils/meson.build
new file:   src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.cc
new file:   src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.h

src/lib/dhcpsrv/cfg_subnets4.cc
src/lib/dhcpsrv/cfg_subnets6.cc
src/lib/dhcpsrv/iterative_allocator.h
src/lib/dhcpsrv/meson.build
src/lib/dhcpsrv/sflq_allocator.cc [new file with mode: 0644]
src/lib/dhcpsrv/sflq_allocator.h [new file with mode: 0644]
src/lib/dhcpsrv/tests/meson.build
src/lib/dhcpsrv/tests/sflq_allocator_unittest.cc [new file with mode: 0644]
src/lib/dhcpsrv/testutils/meson.build
src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.cc [new file with mode: 0644]
src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.h [new file with mode: 0644]

index 13286bb6d4c8bdf471cfb2d3d6a976fe1f9bc91d..f1bd1cf18415280957c7f0b7152055985aa46796 100644 (file)
@@ -11,6 +11,7 @@
 #include <dhcpsrv/cfg_subnets4.h>
 #include <dhcpsrv/dhcpsrv_log.h>
 #include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/sflq_allocator.h>
 #include <dhcpsrv/shared_network.h>
 #include <dhcpsrv/subnet_id.h>
 #include <asiolink/io_address.h>
@@ -612,6 +613,7 @@ CfgSubnets4::updateStatistics() {
 
 void
 CfgSubnets4::initAllocatorsAfterConfigure() {
+    SharedFlqAllocator::setInUse(false);
     for (auto const& subnet : subnets_) {
         subnet->initAllocatorsAfterConfigure();
     }
index fc9f8dbfb584e252fb586cb50fa4d5db423621b5..c7afed60e8786921a001f8b5d4e3eb08bd4257b6 100644 (file)
@@ -10,6 +10,7 @@
 #include <asiolink/addr_utilities.h>
 #include <dhcpsrv/cfg_subnets6.h>
 #include <dhcpsrv/dhcpsrv_log.h>
+#include <dhcpsrv/sflq_allocator.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcpsrv/subnet_id.h>
 #include <stats/stats_mgr.h>
@@ -572,6 +573,7 @@ CfgSubnets6::updateStatistics() {
 
 void
 CfgSubnets6::initAllocatorsAfterConfigure() {
+    SharedFlqAllocator::setInUse(false);
     for (auto const& subnet : subnets_) {
         subnet->initAllocatorsAfterConfigure();
     }
index ef0809029948eb7ebcef8e32b8c76f3cc6df5135..1d5f04df9808c03575295ba7deeea6f0999459fc 100644 (file)
@@ -92,7 +92,7 @@ private:
     /// @return allocation state instance for the pool.
     PoolIterativeAllocationStatePtr getPoolState(const PoolPtr& pool) const;
 
-protected:
+public:
 
     /// @brief Returns the next prefix.
     ///
index 04aaae592066086e0498cdabdd7619ee72cb9b1a..202a7a98b4f683f6c01c78adb16fb71a2295ca4a 100644 (file)
@@ -79,6 +79,7 @@ sources = [
     'random_allocator.cc',
     'resource_handler.cc',
     'sanity_checker.cc',
+    'sflq_allocator.cc',
     'shared_network.cc',
     'srv_config.cc',
     'subnet.cc',
@@ -207,6 +208,7 @@ kea_dhcpsrv_headers = [
     'random_allocator.h',
     'resource_handler.h',
     'sanity_checker.h',
+    'sflq_allocator.h',
     'shared_network.h',
     'srv_config.h',
     'subnet.h',
diff --git a/src/lib/dhcpsrv/sflq_allocator.cc b/src/lib/dhcpsrv/sflq_allocator.cc
new file mode 100644 (file)
index 0000000..5b3fa2f
--- /dev/null
@@ -0,0 +1,210 @@
+// Copyright (C) 2026 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/.
+
+#include <config.h>
+
+#include <asiolink/addr_utilities.h>
+#include <dhcpsrv/dhcpsrv_log.h>
+#include <dhcpsrv/sflq_allocator.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/subnet.h>
+#include <util/stopwatch.h>
+#include <limits>
+
+using namespace isc::asiolink;
+using namespace isc::util;
+using namespace std;
+
+namespace isc {
+    bool sflq_in_use_ = false;
+}
+
+namespace isc {
+namespace dhcp {
+
+void
+SharedFlqAllocator::setInUse(bool in_use) {
+    sflq_in_use_ = in_use;
+}
+
+bool
+SharedFlqAllocator::inUse() {
+    return (sflq_in_use_);
+}
+
+SharedFlqAllocator::SharedFlqAllocator(Lease::Type type, const WeakSubnetPtr& subnet)
+    : Allocator(type, subnet), generator_() {
+    random_device rd;
+    generator_.seed(rd());
+}
+
+void
+SharedFlqAllocator::initAfterConfigureInternal() {
+    auto subnet = subnet_.lock();
+    auto const& pools = subnet->getPools(pool_type_);
+    if (pools.empty()) {
+        // If there are no pools there is nothing to do.
+        return;
+    }
+
+    // Set static class flag marking at least one pool is using SFlq.
+    setInUse(true);
+
+    for (const auto& pool : pools) {
+    switch (pool_type_) {
+        case Lease::TYPE_V4:
+            LeaseMgrFactory::instance().sflqCreateFlqPool4(pool->getFirstAddress(),
+                                                           pool->getLastAddress(),
+                                                           subnet->getID(), false);
+            break;
+        case Lease::TYPE_NA:
+            /// @todo guard against large ranges?
+            LeaseMgrFactory::instance().sflqCreateFlqPool6(pool->getFirstAddress(),
+                                                           pool->getLastAddress(),
+                                                           Lease::TYPE_NA, 128,
+                                                           subnet->getID(), false);
+            break;
+        case Lease::TYPE_PD: {
+            auto pdpool = boost::dynamic_pointer_cast<Pool6>(pool);
+            LeaseMgrFactory::instance().sflqCreateFlqPool6(pool->getFirstAddress(),
+                                                           pool->getLastAddress(),
+                                                           Lease::TYPE_PD, pdpool->getLength(),
+                                                           subnet->getID(), false);
+            break;
+        }
+        default:
+            ;
+        }
+    }
+}
+
+IOAddress
+SharedFlqAllocator::pickAddressInternal(const ClientClasses& client_classes,
+                                        const IdentifierBaseTypePtr&,
+                                        const IOAddress&) {
+    // Let's  iterate over the subnet's pools and identify the ones that
+    // meet client class criteria.
+    auto subnet = subnet_.lock();
+    auto const& pools = subnet->getPools(pool_type_);
+    std::vector<PoolPtr> available;
+    for (auto const& pool : pools) {
+        // Check if the pool is allowed for the client's classes.
+        if (pool->clientSupported(client_classes)) {
+            available.push_back(pool);
+        }
+    }
+
+    // Try each pool in random order.
+    while (available.size()) {
+        // Get a random pool from the available ones.
+        auto offset = getRandomNumber(available.size() - 1);
+        auto const& pool = available[offset];
+        switch (pool_type_) {
+        case Lease::TYPE_V4: {
+            // Ask the lease manager for a lease from the pool.
+            auto free_lease = LeaseMgrFactory::instance()
+                              .sflqPickFreeLease4(pool->getFirstAddress(),
+                                                  pool->getLastAddress());
+            if (!free_lease.isV4Zero()) {
+                return (free_lease);
+            }
+
+            break;
+        }
+        case Lease::TYPE_NA:
+        case Lease::TYPE_TA:{
+            auto free_lease = LeaseMgrFactory::instance()
+                              .sflqPickFreeLease6(pool->getFirstAddress(),
+                                                  pool->getLastAddress());
+            if (!free_lease.isV6Zero()) {
+                return (free_lease);
+            }
+
+            break;
+        }
+        case Lease::TYPE_PD:
+            isc_throw(Unexpected, "pickAddressInternal called for Lease::TYPE_PD");
+                break;
+        }
+
+        // Remove the exhausted pool from the list then try another one.
+        available.erase(available.begin() + offset);
+    }
+
+    // No address available.
+    return (pool_type_ == Lease::TYPE_V4 ? IOAddress::IPV4_ZERO_ADDRESS()
+                                         : IOAddress::IPV6_ZERO_ADDRESS());
+}
+
+IOAddress
+SharedFlqAllocator::pickPrefixInternal(const ClientClasses& client_classes,
+                                            Pool6Ptr& /* pool6 */,
+                                            const IdentifierBaseTypePtr&,
+                                            PrefixLenMatchType prefix_length_match,
+                                            const IOAddress&,
+                                            uint8_t hint_prefix_length) {
+    // Let's  iterate over the subnet's pools and identify the ones that
+    // meet client class criteria.
+    auto subnet = subnet_.lock();
+    auto const& pools = subnet->getPools(pool_type_);
+    std::vector<PoolPtr> available;
+    for (auto const& pool : pools) {
+        // Check if the pool is allowed for the client's classes.
+        if (pool->clientSupported(client_classes)) {
+            if (!Allocator::isValidPrefixPool(prefix_length_match, pool,
+                                              hint_prefix_length)) {
+                continue;
+            }
+
+            available.push_back(pool);
+        }
+    }
+
+    // Try each pool in random order.
+    while (available.size()) {
+        // Get a random pool from the available ones.
+        auto offset = getRandomNumber(available.size() - 1);
+        auto const& pool = available[offset];
+        switch(pool_type_) {
+        case Lease::TYPE_V4:
+            isc_throw(Unexpected, "pickAddressInternal called for Lease::TYPE_V4");
+            break;
+        case Lease::TYPE_NA:
+        case Lease::TYPE_TA:
+            isc_throw(Unexpected, "pickAddressInternal called for Lease::TYPE_NA");
+            break;
+        case Lease::TYPE_PD:
+            // Ask the lease manager for a lease from the pool.
+            auto free_lease = LeaseMgrFactory::instance()
+                              .sflqPickFreeLease6(pool->getFirstAddress(),
+                                                  pool->getLastAddress());
+            if (!free_lease.isV6Zero()) {
+                return (free_lease);
+            }
+
+            break;
+        }
+
+        // Remove the exhausted pool from the list then try another one.
+        available.erase(available.begin() + offset);
+    }
+
+    // No address available.
+    return (IOAddress::IPV6_ZERO_ADDRESS());
+}
+
+uint64_t
+SharedFlqAllocator::getRandomNumber(uint64_t limit) {
+    // Take the short path if there is only one number to randomize from.
+    if (limit == 0) {
+        return (0);
+    }
+    std::uniform_int_distribution<uint64_t> dist(0, limit);
+    return (dist(generator_));
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc
diff --git a/src/lib/dhcpsrv/sflq_allocator.h b/src/lib/dhcpsrv/sflq_allocator.h
new file mode 100644 (file)
index 0000000..7840207
--- /dev/null
@@ -0,0 +1,144 @@
+// Copyright (C) 2026 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 SFLQ_ALLOCATOR_H
+#define SFLQ_ALLOCATOR_H
+
+#include <dhcpsrv/allocator.h>
+#include <dhcpsrv/lease.h>
+#include <cstdint>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief An allocator maintaining a shared queue of free leases.
+///
+/// This allocator is part of the Shared Free Lease Queue (SFLQ) Allocation
+/// scheme. The concept is similar to FLQ Alloction but rather than the
+/// creating and maintaining free lease data locally, it is created and
+/// maintained in the lease back end (MySql and PosgreSQL only) where it
+/// can be shared by other servers.
+///
+/// Th allocator relies on stored procedures in the lease back end to
+/// return free leases for a given IP address range (i.e. pool).  This can
+/// greatly reduces the number of queries made to lease back end as only
+/// addresses that are actually free are returned. The allocation engine
+/// must still check for HR conflicts but generally, the maximum number
+/// of queries for a client request is one query per client-qualified
+/// pool rather than one query * the total number of addresses in the
+/// pools.
+///
+/// The SLFQ data tracks the last address picked for each SFLQ pool such
+/// that consecutive queries for the same pool will return a differnent
+/// free address.  This should minimize conflicts with other servers until
+/// the number of free addresses approaches zero.
+///
+/// @TODO The following needs updating once we decide what do about IA_NA.
+///
+/// This allocator should only be used for reasonably small pools due to the
+/// overhead to populate the free leases. A reasonably small pool is an IPv4
+/// pool (including /8) and the prefix delegation pools with similar capacity.
+/// This allocator is not suitable for a typical IPv6 address pool (e.g., /64).
+/// An attempt to populate free leases for such a giant pool would freeze the
+/// server and likely exhaust its memory.
+///
+/// Free leases are populated in a random order.
+class SharedFlqAllocator : public Allocator {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param type specifies the type of allocated leases.
+    /// @param subnet weak pointer to the subnet owning the allocator.
+    SharedFlqAllocator(Lease::Type type, const WeakSubnetPtr& subnet);
+
+    /// @brief Returns the allocator type string.
+    ///
+    /// @return flq string.
+    virtual std::string getType() const {
+        return ("shared-flq");
+    }
+
+    /// @brief Sets the global in-use flag.
+    ///
+    /// @param in_use new value to assign to the in-use flag.
+    static void setInUse(bool in_use);
+
+    /// @brief Returns the global in-use flag.
+    ///
+    /// @return True if at least one subnet in the current configuration
+    /// is using Shared FLQ allocation.
+    static bool inUse();
+
+private:
+
+    /// @brief Performs allocator initialization after server's reconfiguration.
+    ///
+    /// The allocator installs the callbacks in the lease manager to keep track of
+    /// the lease allocations and maintain the free leases queue.
+    virtual void initAfterConfigureInternal();
+
+    /// @brief Populates the queue of free addresses (IPv4 and IPv6).
+    ///
+    /// Instructs lease the laase back end to (re)create SFLQ datam for
+    /// each pool in a subnet.
+    ///
+    /// @param pools collection of pools in the subnet.
+    void populateFreeAddressLeases(const PoolCollection& pools);
+
+    /// @brief Returns next available address from the queue.
+    ///
+    /// Internal thread-unsafe implementation of the @c pickAddress.
+    ///
+    /// @param client_classes list of classes client belongs to.
+    /// @param duid client DUID (ignored).
+    /// @param hint client hint (ignored).
+    ///
+    /// @return next offered address.
+    virtual asiolink::IOAddress pickAddressInternal(const ClientClasses& client_classes,
+                                                    const IdentifierBaseTypePtr& duid,
+                                                    const asiolink::IOAddress& hint);
+
+    /// @brief Returns next available delegated prefix from the queue.
+    ///
+    /// Internal thread-unsafe implementation of the @c pickPrefix.
+    ///
+    /// @param client_classes list of classes client belongs to.
+    /// @param pool the selected pool satisfying all required conditions.
+    /// @param duid Client's DUID.
+    /// @param prefix_length_match type which indicates the selection criteria
+    ///        for the pools relative to the provided hint prefix length
+    /// @param hint Client's hint.
+    /// @param hint_prefix_length the hint prefix length that the client
+    ///        provided. The 0 value means that there is no hint and that any
+    ///        pool will suffice.
+    ///
+    /// @return the next prefix.
+    virtual isc::asiolink::IOAddress
+    pickPrefixInternal(const ClientClasses& client_classes,
+                       Pool6Ptr& pool,
+                       const IdentifierBaseTypePtr& duid,
+                       PrefixLenMatchType prefix_length_match,
+                       const isc::asiolink::IOAddress& hint,
+                       uint8_t hint_prefix_length);
+
+    /// @brief Convenience function returning a random number.
+    ///
+    /// It is used internally by the @c pickAddressInternal and @c pickPrefixInternal
+    /// functions to select a random pool.
+    ///
+    /// @param limit upper bound of the range.
+    /// @returns random number between 0 and limit.
+    uint64_t getRandomNumber(uint64_t limit);
+
+    /// @brief Random generator used by this class.
+    std::mt19937 generator_;
+};
+
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+#endif // SFLQ_ALLOCATOR_H
index 6b5604a7fb4e575bb7f683f185c8c89414654a25..74963f4b703b6527c6370b605f8b09cb60a42973 100644 (file)
@@ -112,6 +112,7 @@ sources = [
     'resource_handler_unittest.cc',
     'run_unittests.cc',
     'sanity_checks_unittest.cc',
+    'sflq_allocator_unittest.cc',
     'shared_network_parser_unittest.cc',
     'shared_network_unittest.cc',
     'shared_networks_list_parser_unittest.cc',
diff --git a/src/lib/dhcpsrv/tests/sflq_allocator_unittest.cc b/src/lib/dhcpsrv/tests/sflq_allocator_unittest.cc
new file mode 100644 (file)
index 0000000..967bf42
--- /dev/null
@@ -0,0 +1,556 @@
+// Copyright (C) 2026 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/.
+
+#include <config.h>
+//#include <database/database_connection.h>
+//#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <asiolink/io_address.h>
+#include <dhcpsrv/sflq_allocator.h>
+#include <dhcpsrv/testutils/sflqtest_lease_mgr.h>
+#include <gtest/gtest.h>
+#include <testutils/gtest_utils.h>
+
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+
+/// @brief Test fixture class for the DHCPv4 SharedFlqAllocator.
+class SharedFlqAllocatorTest4 : public testing::Test {
+public:
+
+    /// @brief Pre-test setup.
+    ///
+    /// Installs the lease manager factory, creates a manager instance
+    /// and initializes a V4 subnet.
+    virtual void SetUp() {
+        LeaseMgrFactory::registerFactory("sflqtest", SflqTestLeaseMgr::factory);
+        ASSERT_NO_THROW_LOG(LeaseMgrFactory::create("type=sflqtest universe=4"));
+        ASSERT_TRUE(LeaseMgrFactory::haveInstance());
+        ASSERT_EQ(LeaseMgrFactory::instance().getType(), "sflqtest");
+        initSubnet4();
+        SharedFlqAllocator::setInUse(false);
+    }
+
+    virtual void TearDown() {
+        SharedFlqAllocator::setInUse(false);
+    }
+
+    /// @brief Initializes the test subnet for V4 tests.
+    void initSubnet4() {
+        static SubnetID id(1);
+        subnet_ = Subnet4::create(IOAddress("192.0.0.0"), 8, 1, 2, 3, id);
+        PoolPtr pool(new Pool4(IOAddress("192.0.1.0"), IOAddress("192.0.1.1")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("one");
+        subnet_->addPool(pool);
+
+        pool = PoolPtr(new Pool4(IOAddress("192.0.2.0"), IOAddress("192.0.2.1")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("two");
+        subnet_->addPool(pool);
+
+        pool = PoolPtr(new Pool4(IOAddress("192.0.3.0"), IOAddress("192.0.3.1")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("three");
+        subnet_->addPool(pool);
+    }
+
+    /// @brief Subnet used in tests.
+    SubnetPtr subnet_;
+};
+
+// Test that the allocator returns the correct type.
+TEST_F(SharedFlqAllocatorTest4, getType) {
+    SharedFlqAllocator alloc(Lease::TYPE_V4, subnet_);
+    EXPECT_EQ("shared-flq", alloc.getType());
+}
+
+// Tests initAfterConfigure() function. It should create
+// an SFLQ pool for each pool in the subnet.
+TEST_F(SharedFlqAllocatorTest4, initAfterConfigure) {
+    SharedFlqAllocator alloc(Lease::TYPE_V4, subnet_);
+
+    EXPECT_FALSE(SharedFlqAllocator::inUse());
+    EXPECT_NO_THROW(alloc.initAfterConfigure());
+    EXPECT_TRUE(SharedFlqAllocator::inUse());
+
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+
+    for (auto pool : subnet_->getPools(Lease::TYPE_V4)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+    }
+}
+
+// Exercises ShareFlqAllocator::pickAddressInternal() for a V4 subnet.
+TEST_F(SharedFlqAllocatorTest4, pickAddress) {
+    IOAddress zero_address = IOAddress::IPV4_ZERO_ADDRESS();
+    SharedFlqAllocator alloc(Lease::TYPE_V4, subnet_);
+
+    EXPECT_NO_THROW(alloc.initAfterConfigure());
+
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+    for (auto pool : subnet_->getPools(Lease::TYPE_V4)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+    }
+
+    // Verify that all addresses can be picked with client_class = 'ALL'.
+    // We use a set to collect the picked addresses. This way we do not
+    // rely on the order they are picked but can still verify they
+    // all get picked.
+    std::set<IOAddress> picked;
+    ClientClasses client_classes;
+    client_classes.insert("ALL");
+    for (int i = 0; i < 6; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 6);
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.1.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.1.1")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.2.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.2.1")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.3.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.3.1")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+
+    // Verify that only pool 2 addresses are picked for class "two"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("two");
+    for (int i = 0; i < 2; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address))
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 2);
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.2.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.2.1")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+
+    // Verify that only pool 1 and 3 addresses are picked for classes "one" or "three"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("one");
+    client_classes.insert("three");
+    for (int i = 0; i < 4; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 4);
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.1.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.1.1")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.3.0")));
+    ASSERT_TRUE(picked.contains(IOAddress("192.0.3.1")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+}
+
+/// @brief Test fixture class for the DHCPv6 SharedFlqAllocator.
+class SharedFlqAllocatorTest6 : public testing::Test {
+public:
+
+    /// @brief Pre-test setup.
+    ///
+    /// Installs the lease manager factory, creates a manager instance
+    /// and initializes a V4 subnet.
+    virtual void SetUp() {
+        LeaseMgrFactory::registerFactory("sflqtest", SflqTestLeaseMgr::factory);
+        ASSERT_NO_THROW_LOG(LeaseMgrFactory::create("type=sflqtest universe=6"));
+        ASSERT_TRUE(LeaseMgrFactory::haveInstance());
+        ASSERT_EQ(LeaseMgrFactory::instance().getType(), "sflqtest");
+        initSubnet6();
+    }
+
+    /// @brief Initializes the test subnet for V4 tests.
+    void initSubnet6() {
+        static SubnetID id(1);
+        subnet_ = Subnet6::create(IOAddress("3001::"), 64, 0, 0, 0, 0 , id);
+        PoolPtr pool(new Pool6(Lease::TYPE_NA, IOAddress("3001::10"), IOAddress("3001::11")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("one");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("3001::20"), IOAddress("3001::21")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("two");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_NA, IOAddress("3001::30"), IOAddress("3001::31")));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("three");
+        subnet_->addPool(pool);
+
+        // Now add PD pools to match classes, we use same length to make it
+        // verification easy .
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("2001::10"), 127, 128));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("one");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("2001::20"), 127, 128));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("two");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("2001::30"), 127, 128));
+        pool->allowClientClass("ALL");
+        pool->allowClientClass("three");
+        subnet_->addPool(pool);
+
+        // Now add PD pools of with low, middle, high for testing prefix
+        // hint logic.
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("4001:10::"), 124, 124));
+        pool->allowClientClass("LENGTH_HINT");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("4001:20::"), 124, 126));
+        pool->allowClientClass("LENGTH_HINT");
+        subnet_->addPool(pool);
+
+        pool.reset(new Pool6(Lease::TYPE_PD, IOAddress("4001:30::"), 124, 128));
+        pool->allowClientClass("LENGTH_HINT");
+        subnet_->addPool(pool);
+    }
+
+    /// @brief Subnet used in tests.
+    SubnetPtr subnet_;
+};
+
+// Test that the allocator returns the correct type.
+TEST_F(SharedFlqAllocatorTest6, getType) {
+    {
+        SharedFlqAllocator alloc(Lease::TYPE_NA, subnet_);
+        EXPECT_EQ("shared-flq", alloc.getType());
+    }
+
+    {
+        SharedFlqAllocator alloc(Lease::TYPE_PD, subnet_);
+        EXPECT_EQ("shared-flq", alloc.getType());
+    }
+}
+
+// Tests initAfterConfigure() for V6/TYPE_NA pools.
+TEST_F(SharedFlqAllocatorTest6, initAfterConfigureNA) {
+    SharedFlqAllocator alloc(Lease::TYPE_NA, subnet_);
+
+    EXPECT_FALSE(SharedFlqAllocator::inUse());
+    EXPECT_NO_THROW(alloc.initAfterConfigure());
+    EXPECT_TRUE(SharedFlqAllocator::inUse());
+
+    // Verify the NA pools.
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+
+    for (auto pool : subnet_->getPools(Lease::TYPE_NA)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+        ASSERT_EQ(sflq_pool->lease_type_, Lease::TYPE_NA);
+    }
+}
+
+// Exercises ShareFlqAllocator::pickAddressInternal() for V6/TYPE_NA
+// using various class matches.
+TEST_F(SharedFlqAllocatorTest6, pickAddress) {
+    IOAddress zero_address = IOAddress::IPV6_ZERO_ADDRESS();
+
+    SharedFlqAllocator alloc(Lease::TYPE_NA, subnet_);
+
+    ASSERT_NO_THROW_LOG(alloc.initAfterConfigure());
+
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+    for (auto pool : subnet_->getPools(Lease::TYPE_NA)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+    }
+
+    // Verify that all addresses can be picked with client_class = 'ALL'.
+    // We use a set to collect the picked addresses. This way we do not
+    // rely on the order they are picked but can still verify they
+    // all get picked.
+    std::set<IOAddress> picked;
+    ClientClasses client_classes;
+    client_classes.insert("ALL");
+    for (int i = 0; i < 6; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 6);
+    ASSERT_TRUE(picked.contains(IOAddress("3001::10")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::11")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::20")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::21")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::30")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::31")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+
+    // Verify that only pool 2 addresses are picked for class "two"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("two");
+    for (int i = 0; i < 2; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address))
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 2);
+    ASSERT_TRUE(picked.contains(IOAddress("3001::20")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::21")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+
+    // Verify that only pool 1 and 3 addresses are picked for classes "one" or "three"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("one");
+    client_classes.insert("three");
+    for (int i = 0; i < 4; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickAddress(client_classes,
+                                              IdentifierBaseTypePtr(),
+                                              zero_address));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 4);
+    ASSERT_TRUE(picked.contains(IOAddress("3001::10")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::11")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::30")));
+    ASSERT_TRUE(picked.contains(IOAddress("3001::31")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickAddress(client_classes, IdentifierBaseTypePtr(), zero_address),
+              zero_address);
+}
+
+// Tests initAfterConfigure() for V6/TYPE_PD pools.
+TEST_F(SharedFlqAllocatorTest6, initAfterConfigurePD) {
+    SharedFlqAllocator alloc(Lease::TYPE_PD, subnet_);
+
+    ASSERT_NO_THROW_LOG(alloc.initAfterConfigure());
+
+    // Verify the PD pools.
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+
+    for (auto pool : subnet_->getPools(Lease::TYPE_PD)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+        ASSERT_EQ(sflq_pool->lease_type_, Lease::TYPE_PD);
+    }
+}
+
+// Exercises ShareFlqAllocator::pickPrefixInternal() V6/TYPE_PD using
+// various class matches.
+TEST_F(SharedFlqAllocatorTest6, pickPrefix) {
+    IOAddress zero_address = IOAddress::IPV6_ZERO_ADDRESS();
+
+    SharedFlqAllocator alloc(Lease::TYPE_PD, subnet_);
+
+    EXPECT_NO_THROW(alloc.initAfterConfigure());
+
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+
+    // Verify we have the expected SFLQ PD pools.
+    for (auto pool : subnet_->getPools(Lease::TYPE_PD)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+    }
+
+    // Verify that all addresses can be picked with client_class = 'ALL'.
+    // We use a set to collect the picked addresses. This way we do not
+    // rely on the order they are picked but can still verify they
+    // all get picked.
+    std::set<IOAddress> picked;
+    ClientClasses client_classes;
+    client_classes.insert("ALL");
+    auto dummy = Pool6Ptr();
+    for (int i = 0; i < 6; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickPrefix(client_classes,
+                                             dummy,
+                                             IdentifierBaseTypePtr(),
+                                             Allocator::PREFIX_LEN_EQUAL,
+                                             zero_address,
+                                             0));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 6);
+    ASSERT_TRUE(picked.contains(IOAddress("2001::10")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::11")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::20")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::21")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::30")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::31")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickPrefix(client_classes, dummy, IdentifierBaseTypePtr(),
+                               Allocator::PREFIX_LEN_EQUAL, zero_address, 0),
+               zero_address);
+
+    // Verify that only pool 2 addresses are picked for class "two"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("two");
+    for (int i = 0; i < 2; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address =
+                            alloc.pickPrefix(client_classes,
+                                             dummy,
+                                             IdentifierBaseTypePtr(),
+                                             Allocator::PREFIX_LEN_EQUAL,
+                                             zero_address,
+                                             0));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 2);
+    ASSERT_TRUE(picked.contains(IOAddress("2001::20")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::21")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickPrefix(client_classes, dummy, IdentifierBaseTypePtr(),
+                               Allocator::PREFIX_LEN_EQUAL, zero_address, 0),
+               zero_address);
+
+    // Verify that only pool 1 and 3 addresses are picked for classes "one" or "three"
+    slm.repopulateFlqPools();
+    picked.clear();
+    client_classes.clear();
+    client_classes.insert("one");
+    client_classes.insert("three");
+    for (int i = 0; i < 4; ++i) {
+        IOAddress picked_address = zero_address;
+        ASSERT_NO_THROW_LOG(picked_address = alloc.pickPrefix(client_classes,
+                                                              dummy,
+                                                              IdentifierBaseTypePtr(),
+                                                              Allocator::PREFIX_LEN_EQUAL,
+                                                              zero_address,
+                                                              0));
+        ASSERT_NE(picked_address, zero_address);
+        picked.emplace(picked_address);
+    }
+
+    ASSERT_EQ(picked.size(), 4);
+    ASSERT_TRUE(picked.contains(IOAddress("2001::10")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::11")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::30")));
+    ASSERT_TRUE(picked.contains(IOAddress("2001::31")));
+
+    // Verify an additional pick returns zero address.
+    ASSERT_EQ(alloc.pickPrefix(client_classes, dummy, IdentifierBaseTypePtr(),
+                               Allocator::PREFIX_LEN_EQUAL, zero_address, 0),
+               zero_address);
+}
+
+// Exercises ShareFlqAllocator::pickPrefixInternal() V6/TYPE_PD
+// when specifying a prefix length mode and hint.
+TEST_F(SharedFlqAllocatorTest6, pickPrefixLenHint) {
+    IOAddress zero_address = IOAddress::IPV6_ZERO_ADDRESS();
+
+    SharedFlqAllocator alloc(Lease::TYPE_PD, subnet_);
+
+    EXPECT_NO_THROW(alloc.initAfterConfigure());
+
+    SflqTestLeaseMgr& slm = dynamic_cast<SflqTestLeaseMgr&>(LeaseMgrFactory::instance());
+
+    // Verify we have the expected SFLQ PD pools.
+    for (auto pool : subnet_->getPools(Lease::TYPE_PD)) {
+        auto sflq_pool = slm.findPool(pool->getFirstAddress(), pool->getLastAddress());
+        ASSERT_TRUE(sflq_pool) << "no sflq pool for: " << pool->toText();
+    }
+
+    auto dummy = Pool6Ptr();
+    ClientClasses client_classes;
+    client_classes.insert("LENGTH_HINT");
+
+    IOAddress picked_address = zero_address;
+    ASSERT_NO_THROW_LOG(picked_address = alloc.pickPrefix(client_classes,
+                                                          dummy,
+                                                          IdentifierBaseTypePtr(),
+                                                          Allocator::PREFIX_LEN_EQUAL,
+                                                          zero_address,
+                                                          126));
+    EXPECT_EQ(picked_address, IOAddress("4001:20::"));
+
+    ASSERT_NO_THROW_LOG(picked_address = alloc.pickPrefix(client_classes,
+                                                          dummy,
+                                                          IdentifierBaseTypePtr(),
+                                                          Allocator::PREFIX_LEN_LOWER,
+                                                          zero_address,
+                                                          126));
+    EXPECT_EQ(picked_address, IOAddress("4001:10::"));
+
+    ASSERT_NO_THROW_LOG(picked_address = alloc.pickPrefix(client_classes,
+                                                          dummy,
+                                                          IdentifierBaseTypePtr(),
+                                                          Allocator::PREFIX_LEN_HIGHER,
+                                                          zero_address,
+                                                          126));
+    EXPECT_EQ(picked_address, IOAddress("4001:30::"));
+}
+
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
index c0b5c5702715a4c8f4191ecd2549743722ce763e..b316434020a35589acb2dfe01b19253850191b31 100644 (file)
@@ -16,6 +16,7 @@ sources = [
     'host_data_source_utils.cc',
     'lease_file_io.cc',
     'memory_host_data_source.cc',
+    'sflqtest_lease_mgr.cc',
     'test_config_backend_dhcp4.cc',
     'test_config_backend_dhcp6.cc',
     'test_utils.cc',
diff --git a/src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.cc b/src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.cc
new file mode 100644 (file)
index 0000000..3c4f591
--- /dev/null
@@ -0,0 +1,156 @@
+// Copyright (C) 2026 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/.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcpsrv/iterative_allocator.h>
+#include <sflqtest_lease_mgr.h>
+
+using namespace isc::asiolink;
+using namespace isc::db;
+using namespace isc::dhcp;
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+SflqPool::SflqPool(asiolink::IOAddress start_address,
+                   asiolink::IOAddress end_address,
+                   SubnetID subnet_id,
+                   Lease::Type lease_type /* = Lease::TYPE_V4 */,
+                   uint8_t delegated_len /* = 1 */)
+    : start_address_(start_address), end_address_(end_address),
+      subnet_id_(subnet_id), lease_type_(lease_type),
+      delegated_len_(delegated_len) {
+    repopulateFreeLeases();
+}
+
+IOAddress
+SflqPool::zeroAddress() {
+    if (lease_type_ == Lease::TYPE_V4) {
+        return (IOAddress::IPV4_ZERO_ADDRESS());
+    }
+
+    return (IOAddress::IPV6_ZERO_ADDRESS());
+}
+
+void
+SflqPool::repopulateFreeLeases() {
+    // Populate list of free leases with all addresses in
+    // the pool.  For purposes of testing the SharedFlqAllocator
+    // class we don't care about actual leases.
+    IOAddress next_address = start_address_;
+    free_addresses_.clear();
+    while (next_address <= end_address_) {
+        free_addresses_.push_back(next_address);
+        next_address = IterativeAllocator::increaseAddress(next_address,
+                                                           (lease_type_ == Lease::TYPE_PD),
+                                                           delegated_len_);
+    }
+}
+
+IOAddress
+SflqPool::popFreeAddress() {
+    if (free_addresses_.empty()) {
+        return (zeroAddress());
+    }
+
+    IOAddress free_address = free_addresses_.front();
+    free_addresses_.pop_front();
+    return (free_address);
+}
+
+TrackingLeaseMgrPtr
+SflqTestLeaseMgr::factory(const DatabaseConnection::ParameterMap& params) {
+    return (TrackingLeaseMgrPtr(new SflqTestLeaseMgr(params)));
+}
+
+SflqTestLeaseMgr::SflqTestLeaseMgr(const DatabaseConnection::ParameterMap& params)
+    : ConcreteLeaseMgr(params) {
+}
+
+SflqTestLeaseMgr::~SflqTestLeaseMgr() {
+}
+
+bool
+SflqTestLeaseMgr::sflqCreateFlqPool4(IOAddress start_address, IOAddress end_address,
+                                     SubnetID subnet_id, bool recreate) {
+    auto sflq_pool = findPool(start_address, end_address);
+    if (sflq_pool && recreate) {
+        sflq_pool->repopulateFreeLeases();
+    }
+
+    // Create the pool and add it to the list of pools.
+    sflq_pool.reset(new SflqPool(start_address, end_address, subnet_id));
+    sflq_pools_.push_back(sflq_pool);
+    return(true);
+}
+
+IOAddress
+SflqTestLeaseMgr::sflqPickFreeLease4(IOAddress start_address, IOAddress end_address) {
+    auto sflq_pool = findPool(start_address, end_address);
+    if (!sflq_pool) {
+        return (IOAddress::IPV4_ZERO_ADDRESS());
+    }
+
+    return (sflq_pool->popFreeAddress());
+}
+
+bool
+SflqTestLeaseMgr::sflqCreateFlqPool6(IOAddress start_address, IOAddress end_address,
+                                     Lease::Type lease_type, uint8_t delegated_len,
+                                     SubnetID subnet_id, bool recreate) {
+    auto sflq_pool = findPool(start_address, end_address);
+    if (sflq_pool && recreate) {
+        sflq_pool->repopulateFreeLeases();
+    }
+
+    // Create the pool and add it to the list of pools.
+    sflq_pool.reset(new SflqPool(start_address, end_address, subnet_id,
+                                 lease_type, delegated_len));
+    sflq_pools_.push_back(sflq_pool);
+    return(true);
+}
+
+IOAddress
+SflqTestLeaseMgr::sflqPickFreeLease6(IOAddress start_address, IOAddress end_address) {
+    auto sflq_pool = findPool(start_address, end_address);
+    if (!sflq_pool) {
+        return (IOAddress::IPV6_ZERO_ADDRESS());
+    }
+
+    return (sflq_pool->popFreeAddress());
+}
+
+SflqPoolPtr
+SflqTestLeaseMgr::findPool(IOAddress start_address, IOAddress end_address) {
+    for (auto pool : sflq_pools_) {
+        if (pool->start_address_ == start_address &&
+            pool->end_address_ == end_address) {
+            return (pool);
+        }
+    }
+
+    return (SflqPoolPtr());
+}
+
+void
+SflqTestLeaseMgr::repopulateFlqPools() {
+    for (auto pool : sflq_pools_) {
+        pool->repopulateFreeLeases();
+    }
+}
+
+std::string
+SflqTestLeaseMgr::getType() const {
+    return (std::string("sflqtest"));
+}
+
+} // end of namespace isc::dhcp::test
+} // end of namespace isc::dhcp
+} // end of namespace isc
diff --git a/src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.h b/src/lib/dhcpsrv/testutils/sflqtest_lease_mgr.h
new file mode 100644 (file)
index 0000000..d2c48a5
--- /dev/null
@@ -0,0 +1,182 @@
+// Copyright (C) 2023-2025 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 SFLQ_TEST_LEASE_MGR_H
+#define SFLQ_TEST_LEASE_MGR_H
+
+#include <config.h>
+
+#include <database/database_connection.h>
+#include <dhcpsrv/testutils/concrete_lease_mgr.h>
+#include <list>
+#include <utility>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Mock Shared FLQ pool and lease data.
+///
+/// Takes the place of flq_poolX and free_leaseX tables.
+struct SflqPool {
+
+    /// @brief Constructor.
+    ///
+    /// @param start_address first address in the pool.
+    /// @param last_addresss last address in the pool.
+    /// @param subnet_id id of the subnet to which the pool belongs.
+    /// @param recreate when true, the pool is recreated if it already exits.
+    /// @param lease_type Lease::TYPE_V4, TYPE_NA, or TYPE_PD
+    /// @param delegated_len bit length of the address/prefix to be leases. For
+    /// TYPE_NA this parameter should be 128.
+    SflqPool(asiolink::IOAddress start_address_,
+             asiolink::IOAddress end_address_,
+             SubnetID subnet_id_,
+             Lease::Type lease_type = Lease::TYPE_V4,
+             uint8_t delegated_len  = 1);
+
+    /// @brief Desructor.
+    ~SflqPool(){};
+
+    /// @brief Refills the free lease list with all leases in the pool.
+    void repopulateFreeLeases();
+
+    /// @brief Removes and returns an address from the front of the free
+    /// address list.
+    ///
+    /// @return An address or '0.0.0.0'/'::' is there are none.
+    asiolink::IOAddress popFreeAddress();
+
+    /// @brief Convenience function that returns a pool-appropriate
+    /// empty IOAddress.
+    ///
+    /// @return IPV4_ZERO_ADDRESS() if pool leas type is TYPE_V4,
+    /// IPV6_ZERO_ADDRESS() otherwise.
+    asiolink::IOAddress zeroAddress();
+
+    /// @brief First address in the pool.
+    asiolink::IOAddress start_address_;
+
+    /// @brief Last address in the pool.
+    asiolink::IOAddress end_address_;
+
+    /// @brief Id of the subnet to which the pool belongs.
+    SubnetID subnet_id_;
+
+    /// @brief Lease type of pool.
+    Lease::Type lease_type_;
+
+    /// @brief Length of the address/prefix to be leases.
+    uint8_t delegated_len_;
+
+    /// @brief List of free addresses in the pool.
+    std::list<asiolink::IOAddress> free_addresses_;
+
+};
+
+/// @brief A pointer to a SflqPool.
+typedef boost::shared_ptr<SflqPool> SflqPoolPtr;
+
+/// @brief A list of SflqPoolPtrs.
+typedef std::list<SflqPoolPtr> SflqPoolCollection;
+
+// This is a concrete implementation of a Lease database.  It does not do
+// anything useful and is used for abstract LeaseMgr class testing.
+class SflqTestLeaseMgr : public ConcreteLeaseMgr {
+public:
+
+    /// @brief The sole lease manager constructor
+    ///
+    /// dbconfig is a generic way of passing parameters. Parameters
+    /// are passed in the "name=value" format, separated by spaces.
+    /// Values may be enclosed in double quotes, if needed.
+    SflqTestLeaseMgr(const db::DatabaseConnection::ParameterMap&);
+
+    /// @brief Destructor
+    virtual ~SflqTestLeaseMgr();
+
+    /// @brief Factory for creating a SlfqTestLeaseMgr.
+    ///
+    /// The only required parameters are "type=sflqtest" adn "universe=4"
+    /// or "universe=6".
+    ///
+    /// @param params Connection parameters for creating the manager.
+    static TrackingLeaseMgrPtr factory(const db::DatabaseConnection
+                                             ::ParameterMap& params);
+
+    /// @brief Creates a v4 SFLQ Pool
+    ///
+    /// @param start_address first address in the pool.
+    /// @param last_addresss last address in the pool.
+    /// @param subnet_id id of the subnet to which the pool belongs.
+    /// @param recreate when true, the pool is recreated if it already exits.
+    ///
+    /// @return True if the pool is (re)created, false it if already exists.
+    virtual bool sflqCreateFlqPool4(asiolink::IOAddress start_address,
+                                    asiolink::IOAddress end_address,
+                                    SubnetID subnet_id, bool recreate = false);
+
+    /// @brief Finds a free V4 address within the given pool range.
+    ///
+    /// @param start_address first address in the pool.
+    /// @param last_addresss last address in the pool.
+    ///
+    /// @return A free V4 address or IOAddress::IPV4_ZERO_ADDRESS().
+    virtual asiolink::IOAddress sflqPickFreeLease4(asiolink::IOAddress start_address,
+                                                   asiolink::IOAddress end_address);
+
+    /// @brief Calls stored procedure to create an SFLQ pool for v6.
+    ///
+    /// @param start_address first address/prefix in the pool.
+    /// @param last_addresss last address/prefix in the pool.
+    /// @param lease_type TYPE_NA or TYPE_PD.
+    /// @param delegated_len bit length of the address/prefix to be leases. For
+    /// TYPE_NA this parameter should be 128.
+    /// @param subnet_id id of the subnet to which the pool belongs.
+    /// @param recreate when true, the pool is recreated if it already exits.
+    ///
+    /// @return True if the pool is (re)created, false it if already exists.
+    virtual bool sflqCreateFlqPool6(asiolink::IOAddress start_address,
+                                    asiolink::IOAddress end_address,
+                                    Lease::Type lease_type, uint8_t delegated_len,
+                                    SubnetID subnet_id, bool recreate = false);
+
+    /// @brief Finds a free V6 address/prefix within the given pool range.
+    ///
+    /// @param start_address first address in the pool.
+    /// @param last_addresss last address in the pool.
+    ///
+    /// @return A free V6 address/prefix or IOAddress::IPV6_ZERO_ADDRESS().
+    virtual asiolink::IOAddress sflqPickFreeLease6(asiolink::IOAddress start_address,
+                                                   asiolink::IOAddress end_address);
+
+    /// @brief Finds an SflqPool in the list of SflqPools
+    ///
+    /// @param start_address first address in the pool.
+    /// @param last_addresss last address in the pool.
+    ///
+    /// @return Pointer to the desired pool are an empty pointer.
+    SflqPoolPtr findPool(asiolink::IOAddress start_address, asiolink::IOAddress end_address);
+
+    /// @brief Refills the free lease lists for all SFLQ pools.
+    void repopulateFlqPools();
+
+    /// @brief Returns backend type.
+    ///
+    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    ///
+    /// @return Type of the backend.
+    virtual std::string getType() const override;
+
+    /// @brief Collection of SFLQ pools that have been created.
+    SflqPoolCollection sflq_pools_;
+};
+
+} // end of namespace isc::dhcp::test
+} // end of namespace isc::dhcp
+} // end of namespace isc
+
+#endif // SFLQ_TEST_LEASE_MGR_H