]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3693] Skip reclamation when service is disabled
authorThomas Markwalder <tmark@isc.org>
Tue, 11 Mar 2025 11:51:48 +0000 (07:51 -0400)
committerThomas Markwalder <tmark@isc.org>
Tue, 11 Mar 2025 11:51:48 +0000 (07:51 -0400)
/src/bin/dhcp4/ctrl_dhcp4_srv.cc
    ControlledDhcpv4Srv::reclaimExpiredLeases()
    ControlledDhcpv4Srv::deleteExpiredReclaimedLeases()
    - do nothing but reschedule the timer when DHCP service
    is disabled

/src/bin/dhcp4/dhcp4_messages.mes
    DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED - new log message

/src/bin/dhcp4/tests/kea_controller_unittest.cc
    TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) - new test

/src/bin/dhcp6/ctrl_dhcp6_srv.cc
    ControlledDhcpv6Srv::reclaimExpiredLeases()
    ControlledDhcpv6Srv::deleteExpiredReclaimedLeases()
    - do nothing but reschedule the timer when DHCP service
    is disabled

/src/bin/dhcp6/dhcp6_messages.mes
    DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED  - new log message

/src/bin/dhcp6/tests/kea_controller_unittest.cc
    TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) - new test

changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation [new file with mode: 0644]
src/bin/dhcp4/ctrl_dhcp4_srv.cc
src/bin/dhcp4/dhcp4_messages.cc
src/bin/dhcp4/dhcp4_messages.h
src/bin/dhcp4/dhcp4_messages.mes
src/bin/dhcp4/tests/kea_controller_unittest.cc
src/bin/dhcp6/ctrl_dhcp6_srv.cc
src/bin/dhcp6/dhcp6_messages.cc
src/bin/dhcp6/dhcp6_messages.h
src/bin/dhcp6/dhcp6_messages.mes
src/bin/dhcp6/tests/kea_controller_unittest.cc

diff --git a/changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation b/changelog_unreleased/3693-if-dhcp-is-disabled-then-do-not-do-lease-reclamation
new file mode 100644 (file)
index 0000000..fc622fb
--- /dev/null
@@ -0,0 +1,5 @@
+[func]         tmark
+       Lease reclamation is now skipped and rescheduled
+    while DHCP service is disabled. This applies to
+    both kea-dhcp4 and kea-dhcp6.
+       (Gitlab #3693)
index 1d2707bcad056d8c7d5845f2d076fdb179e34092..a5c9371fe8bb020170e9b1e31e44ae2a726ccd0d 100644 (file)
@@ -1610,9 +1610,15 @@ ControlledDhcpv4Srv::reclaimExpiredLeases(const size_t max_leases,
                                           const bool remove_lease,
                                           const uint16_t max_unwarned_cycles) {
     try {
-        server_->alloc_engine_->reclaimExpiredLeases4(max_leases, timeout,
-                                                      remove_lease,
-                                                      max_unwarned_cycles);
+        if (network_state_->isServiceEnabled()) {
+            server_->alloc_engine_->reclaimExpiredLeases4(max_leases, timeout,
+                                                          remove_lease,
+                                                          max_unwarned_cycles);
+        } else {
+            LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED)
+                .arg(CfgMgr::instance().getCurrentCfg()->
+                 getCfgExpiration()->getReclaimTimerWaitTime());
+        }
     } catch (const std::exception& ex) {
         LOG_ERROR(dhcp4_logger, DHCP4_RECLAIM_EXPIRED_LEASES_FAIL)
             .arg(ex.what());
@@ -1623,7 +1629,10 @@ ControlledDhcpv4Srv::reclaimExpiredLeases(const size_t max_leases,
 
 void
 ControlledDhcpv4Srv::deleteExpiredReclaimedLeases(const uint32_t secs) {
-    server_->alloc_engine_->deleteExpiredReclaimedLeases4(secs);
+    if (network_state_->isServiceEnabled()) {
+        server_->alloc_engine_->deleteExpiredReclaimedLeases4(secs);
+    }
+
     // We're using the ONE_SHOT timer so there is a need to re-schedule it.
     TimerMgr::instance()->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME);
 }
index d097cd398019ef0377b5e103b5569b8df63f7141..578d058ee8b52680eba4787b4f27162a7a35b10a 100644 (file)
@@ -141,6 +141,7 @@ extern const isc::log::MessageID DHCP4_POST_ALLOCATION_NAME_UPDATE_FAIL = "DHCP4
 extern const isc::log::MessageID DHCP4_QUERY_DATA = "DHCP4_QUERY_DATA";
 extern const isc::log::MessageID DHCP4_QUERY_LABEL = "DHCP4_QUERY_LABEL";
 extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_FAIL = "DHCP4_RECLAIM_EXPIRED_LEASES_FAIL";
+extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED = "DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED";
 extern const isc::log::MessageID DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO = "DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO";
 extern const isc::log::MessageID DHCP4_RELEASE = "DHCP4_RELEASE";
 extern const isc::log::MessageID DHCP4_RELEASE_DELETED = "DHCP4_RELEASE_DELETED";
@@ -319,6 +320,7 @@ const char* values[] = {
     "DHCP4_QUERY_DATA", "%1, packet details: %2",
     "DHCP4_QUERY_LABEL", "received query: %1",
     "DHCP4_RECLAIM_EXPIRED_LEASES_FAIL", "failed to reclaim expired leases: %1",
+    "DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED", "dhcp6 service is currently disabled. Try again in %1 seconds.",
     "DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO", "recovered for query %1 relay agent option from lease %2: %3",
     "DHCP4_RELEASE", "%1: address %2 was released properly.",
     "DHCP4_RELEASE_DELETED", "%1: address %2 was deleted on release.",
index 7c2104fe6accb8c32e282dd36024fe50cb01d8cc..112ee36779a266a86066ae497a148a00f333bc3a 100644 (file)
@@ -142,6 +142,7 @@ extern const isc::log::MessageID DHCP4_POST_ALLOCATION_NAME_UPDATE_FAIL;
 extern const isc::log::MessageID DHCP4_QUERY_DATA;
 extern const isc::log::MessageID DHCP4_QUERY_LABEL;
 extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_FAIL;
+extern const isc::log::MessageID DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED;
 extern const isc::log::MessageID DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO;
 extern const isc::log::MessageID DHCP4_RELEASE;
 extern const isc::log::MessageID DHCP4_RELEASE_DELETED;
index 96d0ea84247b97960e2c53ebcfdd392418203713..05100f7d20fb23257ba3a12c0453c2576a9d8dfa 100644 (file)
@@ -916,6 +916,11 @@ the client and the transaction identification information.
 This error message indicates that the reclaim expired leases operation failed
 and provides the cause of failure.
 
+% DHCP4_RECLAIM_EXPIRED_LEASES_SKIPPED dhcp6 service is currently disabled. Try again in %1 seconds.
+This debug message is emitted when lease reclamation was scheduled to begin
+but skipped because DHCPv6 service was disabled. Reclamation will continue
+to be scheduled according to the configured value of reclaim-timer-wait-time.
+
 % DHCP4_RECOVERED_STASHED_RELAY_AGENT_INFO recovered for query %1 relay agent option from lease %2: %3
 Logged at debug log level 55.
 This debug message indicates that agent options were stashed in the lease for
index 3e4bdc79d87236bd16b90768446b2f22c9e657cb..c800123a64b9ef392eabcb9b7f15f10eeb6b199d 100644 (file)
@@ -188,6 +188,18 @@ public:
         // stub implementation used in tests.
         cb_control_.reset(new TestCBControlDHCPv4());
     }
+
+
+    /// @brief Convenience method that enables or disables DHCP service.
+    ///
+    /// @param enable true to enable service, false to disable it.
+    void enableService(bool enable) {
+        if (enable) {
+            network_state_->enableService(NetworkState::USER_COMMAND);
+        } else {
+            network_state_->disableService(NetworkState::USER_COMMAND);
+        }
+    }
 };
 
 /// @brief test class for Kea configuration backend.
@@ -1103,4 +1115,107 @@ TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) {
 
 #endif
 
+// This test verifies that the DHCP server only reclaims or flushes leases
+// when DHCP6 service is enabled.
+TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) {
+    // This is a basic configuration which enables timers for reclaiming
+    // expired leases and flushing them after 500 seconds since they expire.
+    // Both timers run at 1 second intervals.
+    string config =
+        "{ \"Dhcp4\": {"
+        "\"interfaces-config\": {"
+        "    \"interfaces\": [ ]"
+        "},"
+        "\"lease-database\": {"
+        "     \"type\": \"memfile\","
+        "     \"persist\": false"
+        "},"
+        "\"expired-leases-processing\": {"
+        "     \"reclaim-timer-wait-time\": 1,"
+        "     \"hold-reclaimed-time\": 500,"
+        "     \"flush-reclaimed-timer-wait-time\": 1"
+        "},"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, \n"
+        "\"subnet4\": [ ],"
+        "\"valid-lifetime\": 4000 }"
+        "}";
+    writeFile(TEST_FILE, config);
+
+    // Create an instance of the server and initialize it.
+    boost::scoped_ptr<NakedControlledDhcpv4Srv> srv;
+    ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv4Srv()));
+    ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+    // Create an expired lease. The lease is expired by 40 seconds ago
+    // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed
+    // but shouldn't be flushed in the database because the reclaimed are
+    // held in the database 500 seconds after reclamation, according to the
+    // current configuration.
+    HWAddrPtr hwaddr_expired(new HWAddr(HWAddr::fromText("00:01:02:03:04:05")));
+    Lease4Ptr lease_expired(new Lease4(IOAddress("10.0.0.1"), hwaddr_expired,
+                                       ClientIdPtr(), 60,
+                                       time(NULL) - 100, SubnetID(1)));
+
+    // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds
+    // ago. It should be removed from the lease database when the "flush" timer
+    // goes off.
+    HWAddrPtr hwaddr_reclaimed(new HWAddr(HWAddr::fromText("01:02:03:04:05:06")));
+    Lease4Ptr lease_reclaimed(new Lease4(IOAddress("10.0.0.2"), hwaddr_reclaimed,
+                                         ClientIdPtr(), 60,
+                                         time(NULL) - 1000, SubnetID(1)));
+    lease_reclaimed->state_ = Lease4::STATE_EXPIRED_RECLAIMED;
+
+    // Add leases to the database.
+    LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+    ASSERT_NO_THROW(lease_mgr.addLease(lease_expired));
+    ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed));
+
+    // Make sure they have been added.
+    ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.1")));
+    ASSERT_TRUE(lease_mgr.getLease4(IOAddress("10.0.0.2")));
+
+    // Disable service.
+    srv->enableService(false);
+
+    // Poll the timers for a while to make sure that each of them is executed
+    // at least once.
+    ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+    // Verify that the leases in the database have not been processed.
+    ASSERT_NO_THROW(
+        lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1"))
+    );
+    ASSERT_TRUE(lease_expired);
+    ASSERT_EQ(Lease::STATE_DEFAULT, lease_expired->state_);
+
+    // Second lease should not have been removed.
+    ASSERT_NO_THROW(
+        lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2"))
+    );
+    ASSERT_TRUE(lease_reclaimed);
+    ASSERT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease_reclaimed->state_);
+
+    // Enable service.
+    srv->enableService(true);
+
+    // Poll the timers for a while to make sure that each of them is executed
+    // at least once.
+    ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+    // Verify that the leases in the database have been processed as expected.
+
+    // First lease should be reclaimed, but not removed.
+    ASSERT_NO_THROW(lease_expired = lease_mgr.getLease4(IOAddress("10.0.0.1")));
+    ASSERT_TRUE(lease_expired);
+    EXPECT_TRUE(lease_expired->stateExpiredReclaimed());
+
+    // Second lease should have been removed.
+    ASSERT_NO_THROW(
+        lease_reclaimed = lease_mgr.getLease4(IOAddress("10.0.0.2"));
+    );
+    EXPECT_FALSE(lease_reclaimed);
+}
+
+
 } // End of anonymous namespace
index a5106cd0b8d9da4bc5b88d85af03c6571c078418..d9761793a5e02475f2374b80609531c0fe9da826 100644 (file)
@@ -1396,9 +1396,15 @@ ControlledDhcpv6Srv::reclaimExpiredLeases(const size_t max_leases,
                                           const bool remove_lease,
                                           const uint16_t max_unwarned_cycles) {
     try {
+        if (network_state_->isServiceEnabled()) {
         server_->alloc_engine_->reclaimExpiredLeases6(max_leases, timeout,
                                                       remove_lease,
                                                       max_unwarned_cycles);
+        } else {
+            LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED)
+                .arg(CfgMgr::instance().getCurrentCfg()->
+                     getCfgExpiration()->getReclaimTimerWaitTime());
+        }
     } catch (const std::exception& ex) {
         LOG_ERROR(dhcp6_logger, DHCP6_RECLAIM_EXPIRED_LEASES_FAIL)
             .arg(ex.what());
@@ -1409,7 +1415,10 @@ ControlledDhcpv6Srv::reclaimExpiredLeases(const size_t max_leases,
 
 void
 ControlledDhcpv6Srv::deleteExpiredReclaimedLeases(const uint32_t secs) {
-    server_->alloc_engine_->deleteExpiredReclaimedLeases6(secs);
+    if (network_state_->isServiceEnabled()) {
+        server_->alloc_engine_->deleteExpiredReclaimedLeases6(secs);
+    }
+
     // We're using the ONE_SHOT timer so there is a need to re-schedule it.
     TimerMgr::instance()->setup(CfgExpiration::FLUSH_RECLAIMED_TIMER_NAME);
 }
index 57a50c5ecc8cb2de4c3d8dc8a1f59f733b28b09c..c5d57d7f35bd75fb94ccb01f4b55d41821bb4b11 100644 (file)
@@ -141,6 +141,7 @@ extern const isc::log::MessageID DHCP6_QUERY_DATA = "DHCP6_QUERY_DATA";
 extern const isc::log::MessageID DHCP6_QUERY_LABEL = "DHCP6_QUERY_LABEL";
 extern const isc::log::MessageID DHCP6_RAPID_COMMIT = "DHCP6_RAPID_COMMIT";
 extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_FAIL = "DHCP6_RECLAIM_EXPIRED_LEASES_FAIL";
+extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED = "DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED";
 extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_ADD_FAIL = "DHCP6_REGISTERED_LEASE_ADD_FAIL";
 extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_UPDATE_FAIL = "DHCP6_REGISTERED_LEASE_UPDATE_FAIL";
 extern const isc::log::MessageID DHCP6_RELEASE_NA = "DHCP6_RELEASE_NA";
@@ -314,6 +315,7 @@ const char* values[] = {
     "DHCP6_QUERY_LABEL", "received query: %1",
     "DHCP6_RAPID_COMMIT", "%1: Rapid Commit option received, following 2-way exchange",
     "DHCP6_RECLAIM_EXPIRED_LEASES_FAIL", "failed to reclaim expired leases: %1",
+    "DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED", "dhcp6 service is currently disabled. Try again in %1 seconds.",
     "DHCP6_REGISTERED_LEASE_ADD_FAIL", "error in registered lease add for %1",
     "DHCP6_REGISTERED_LEASE_UPDATE_FAIL", "error in registered lease update for %1: %2",
     "DHCP6_RELEASE_NA", "%1: binding for address %2 and iaid=%3 was released properly",
index 8820f389cbd4d848fb50c52a16ea3248d5537f58..7b5cf18c05a55ded7d88c61928d758ad9b090ca5 100644 (file)
@@ -142,6 +142,7 @@ extern const isc::log::MessageID DHCP6_QUERY_DATA;
 extern const isc::log::MessageID DHCP6_QUERY_LABEL;
 extern const isc::log::MessageID DHCP6_RAPID_COMMIT;
 extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_FAIL;
+extern const isc::log::MessageID DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED;
 extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_ADD_FAIL;
 extern const isc::log::MessageID DHCP6_REGISTERED_LEASE_UPDATE_FAIL;
 extern const isc::log::MessageID DHCP6_RELEASE_NA;
index 6935877a27ae4e0b79154734ca2cdf9b7f93736b..466502cd4c11c283cdc34f5519832fb76a7e619d 100644 (file)
@@ -928,6 +928,11 @@ specifies the client and transaction identification information.
 This error message indicates that the reclaim expired leases operation failed
 and provides the cause of failure.
 
+% DHCP6_RECLAIM_EXPIRED_LEASES_SKIPPED dhcp6 service is currently disabled. Try again in %1 seconds.
+This debug message is emitted when lease reclamation was scheduled to begin
+but skipped because DHCPv6 service was disabled. Reclamation will continue
+to be scheduled according to the configured value of reclaim-timer-wait-time.
+
 % DHCP6_REGISTERED_LEASE_ADD_FAIL error in registered lease add for %1
 This error message indicates that the registered lease add failed and
 provides the address being registered.
index 13af8004bfbf9a68d95130096333635d01702c32..80fb89b37a66e0b4f7707b8d1710f4543a368aec 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2024 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-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
@@ -185,6 +185,17 @@ public:
         // stub implementation used in tests.
         cb_control_.reset(new TestCBControlDHCPv6());
     }
+
+    /// @brief Convenience method that enables or disables DHCP service.
+    ///
+    /// @param enable true to enable service, false to disable it.
+    void enableService(bool enable) {
+        if (enable) {
+            network_state_->enableService(NetworkState::USER_COMMAND);
+        } else {
+            network_state_->disableService(NetworkState::USER_COMMAND);
+        }
+    }
 };
 
 /// @brief test class for Kea configuration backend.
@@ -1090,4 +1101,109 @@ TEST_F(JSONFileBackendMySQLTest, reconfigureBackendMemfileToMySQL) {
 
 #endif
 
+// This test verifies that the DHCP server only reclaims or flushes leases
+// when DHCP6 service is enabled.
+TEST_F(JSONFileBackendTest, reclaimOnlyWhenServiceEnabled) {
+    // This is a basic configuration which enables timers for reclaiming
+    // expired leases and flushing them after 500 seconds since they expire.
+    // Both timers run at 1 second intervals.
+    string config =
+        "{ \"Dhcp6\": {"
+        "\"interfaces-config\": {"
+        "    \"interfaces\": [ ]"
+        "},"
+        "\"lease-database\": {"
+        "     \"type\": \"memfile\","
+        "     \"persist\": false"
+        "},"
+        "\"expired-leases-processing\": {"
+        "     \"reclaim-timer-wait-time\": 1,"
+        "     \"hold-reclaimed-time\": 500,"
+        "     \"flush-reclaimed-timer-wait-time\": 1"
+        "},"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ ],"
+        "\"preferred-lifetime\": 3000, "
+        "\"valid-lifetime\": 4000 }"
+        "}";
+    writeFile(TEST_FILE, config);
+
+    // Create an instance of the server and initialize it.
+    boost::scoped_ptr<NakedControlledDhcpv6Srv> srv;
+    ASSERT_NO_THROW(srv.reset(new NakedControlledDhcpv6Srv()));
+    ASSERT_NO_THROW(srv->init(TEST_FILE));
+
+    // Create an expired lease. The lease is expired by 40 seconds ago
+    // (valid lifetime = 60, cltt = now - 100). The lease will be reclaimed
+    // but shouldn't be flushed in the database because the reclaimed are
+    // held in the database 500 seconds after reclamation, according to the
+    // current configuration.
+    DuidPtr duid_expired(new DUID(DUID::fromText("00:01:02:03:04:05:06").getDuid()));
+    Lease6Ptr lease_expired(new Lease6(Lease::TYPE_NA, IOAddress("3000::1"),
+                                       duid_expired, 1, 50, 60, SubnetID(1)));
+    lease_expired->cltt_ = time(NULL) - 100;
+
+    // Create expired-reclaimed lease. The lease has expired 1000 - 60 seconds
+    // ago. It should be removed from the lease database when the "flush" timer
+    // goes off.
+    DuidPtr duid_reclaimed(new DUID(DUID::fromText("01:02:03:04:05:06:07").getDuid()));
+    Lease6Ptr lease_reclaimed(new Lease6(Lease::TYPE_NA, IOAddress("3000::2"),
+                                         duid_reclaimed, 1, 50, 60, SubnetID(1)));
+    lease_reclaimed->cltt_ = time(NULL) - 1000;
+    lease_reclaimed->state_ = Lease6::STATE_EXPIRED_RECLAIMED;
+
+    // Add leases to the database.
+    LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
+    ASSERT_NO_THROW(lease_mgr.addLease(lease_expired));
+    ASSERT_NO_THROW(lease_mgr.addLease(lease_reclaimed));
+
+    // Make sure they have been added.
+    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1")));
+    ASSERT_TRUE(lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2")));
+
+    // Disable service.
+    srv->enableService(false);
+
+    // Poll the timers for a while to make sure that each of them is executed
+    // at least once.
+    ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+    // Verify that the leases in the database have not been processed.
+    ASSERT_NO_THROW(
+        lease_expired = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1"))
+    );
+    ASSERT_TRUE(lease_expired);
+    ASSERT_EQ(Lease::STATE_DEFAULT, lease_expired->state_);
+
+    // Second lease should not have been removed.
+    ASSERT_NO_THROW(
+        lease_reclaimed = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2"))
+    );
+    ASSERT_TRUE(lease_reclaimed);
+    ASSERT_EQ(Lease::STATE_EXPIRED_RECLAIMED, lease_reclaimed->state_);
+
+    // Enable service.
+    srv->enableService(true);
+
+    // Poll the timers for a while to make sure that each of them is executed
+    // at least once.
+    ASSERT_NO_THROW(runTimersWithTimeout(srv->getIOService(), 5000));
+
+    // Verify that the leases in the database have been processed as expected.
+
+    // First lease should be reclaimed, but not removed.
+    ASSERT_NO_THROW(
+        lease_expired = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::1"))
+    );
+    ASSERT_TRUE(lease_expired);
+    EXPECT_TRUE(lease_expired->stateExpiredReclaimed());
+
+    // Second lease should have been removed.
+    ASSERT_NO_THROW(
+        lease_reclaimed = lease_mgr.getLease6(Lease::TYPE_NA, IOAddress("3000::2"))
+    );
+    EXPECT_FALSE(lease_reclaimed);
+}
+
 } // End of anonymous namespace