Browse Source

[master] Merge branch 'trac3972'

Marcin Siodelski 9 years ago
parent
commit
8a8547aec1

+ 30 - 0
src/bin/dhcp4/dhcp4_hooks.dox

@@ -217,5 +217,35 @@ packet processing. Hook points that are not specific to packet processing
    (e.g. lease was allocated). Setting this flag merely stops the change
    being communicated to the client.
 
+@subsection dhcpv4HooksLease4Expire lease4_expire
+
+- @b Arguments:
+  - name: @b lease4, type: isc::dhcp::Lease4Ptr, direction: <b>in/out</b>
+  - name: @b remove_lease, type: bool, direction: <b>in</b>
+
+- @b Description: this callout is executed for each expired lease when
+  the server performs reclamation of the expired leases. During this
+  process the server executes "lease4_expire" callout, removes the DNS
+  records associated with this lease and finally removes the lease from
+  the database or updates its status to "expired-reclaimed". The "lease4"
+  argument contains the pointer to the lease being reclaimed. The second
+  argument "remove_lease" indicates if the reclaimed leases should be
+  removed from the lease database (if true), or their state should be
+  set to "expired-reclaimed" in the lease database. This argument
+  is only used by the callout if it takes responsibility for the lease
+  reclamation, i.e. it sets the "skip" flag to "true". The "remove_lease"
+  argument is set to "true" if the "flush-reclaimed-timer-wait-time" is
+  set to 0 in the server configuration file.
+
+- <b>Skip flag action</b>: if the callout sets the skip flag, the server
+  will assume that the callout has fully reclaimed the lease, i.e.
+  performed the DNS update and updated the lease in the database. The
+  server will not perform any further actions on the lease for which the
+  skip flag has been set. It is important to note that if the callout
+  sets this flag but fails to reclaim the lease in the database, the
+  reclamation routine will repeatedly process this lease in subsequent
+  runs. Therefore, the implementors of this callout must make sure that
+  skip flag is only set when the lease has been actually reclaimed in the
+  database by the callout.
 
 */

+ 31 - 0
src/bin/dhcp6/dhcp6_hooks.dox

@@ -235,4 +235,35 @@ packet processing. Hook points that are not specific to packet processing
    (e.g. lease was allocated). Setting this flag merely stops the change
    being communicated to the client.
 
+@subsection dhcpv6HooksLease6Expire lease6_expire
+
+- @b Arguments:
+  - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+  - name: @b remove_lease, type: bool, direction: <b>in</b>
+
+- @b Description: this callout is executed for each expired lease when
+  the server performs reclamation of the expired leases. During this
+  process the server executes "lease6_expire" callout, removes the DNS
+  records associated with this lease and finally removes the lease from
+  the database or updates its status to "expired-reclaimed". The "lease6"
+  argument contains the pointer to the lease being reclaimed. The second
+  argument "remove_lease" indicates if the reclaimed leases should be
+  removed from the lease database (if true), or their state should be
+  set to "expired-reclaimed" in the lease database. This argument
+  is only used by the callout if it takes responsibility for the lease
+  reclamation, i.e. it sets the "skip" flag to "true".  The "remove_lease"
+  argument is set to "true" if the "flush-reclaimed-timer-wait-time" is
+  set to 0 in the server configuration file.
+
+- <b>Skip flag action</b>: if the callout sets the skip flag, the server
+  will assume that the callout has fully reclaimed the lease, i.e.
+  performed the DNS update and updated the lease in the database. The
+  server will not perform any further actions on the lease for which the
+  skip flag has been set. It is important to note that if the callout
+  sets this flag but fails to reclaim the lease in the database, the
+  reclamation routine will repeatedly process this lease in subsequent
+  runs. Therefore, the implementors of this callout must make sure that
+  skip flag is only set when the lease has been actually reclaimed in the
+  database by the callout.
+
 */

+ 111 - 35
src/lib/dhcpsrv/alloc_engine.cc

@@ -25,6 +25,8 @@
 #include <dhcpsrv/host.h>
 #include <dhcpsrv/lease_mgr_factory.h>
 #include <dhcp/dhcp6.h>
+#include <hooks/callout_handle.h>
+#include <hooks/hooks_manager.h>
 #include <stats/stats_mgr.h>
 #include <util/stopwatch.h>
 #include <hooks/server_hooks.h>
@@ -51,17 +53,21 @@ namespace {
 struct AllocEngineHooks {
     int hook_index_lease4_select_; ///< index for "lease4_receive" hook point
     int hook_index_lease4_renew_;  ///< index for "lease4_renew" hook point
+    int hook_index_lease4_expire_; ///< index for "lease4_expire" hook point
     int hook_index_lease6_select_; ///< index for "lease6_receive" hook point
     int hook_index_lease6_renew_;  ///< index for "lease6_renew" hook point
     int hook_index_lease6_rebind_; ///< index for "lease6_rebind" hook point
+    int hook_index_lease6_expire_; ///< index for "lease6_expire" hook point
 
     /// Constructor that registers hook points for AllocationEngine
     AllocEngineHooks() {
         hook_index_lease4_select_ = HooksManager::registerHook("lease4_select");
         hook_index_lease4_renew_  = HooksManager::registerHook("lease4_renew");
+        hook_index_lease4_expire_ = HooksManager::registerHook("lease4_expire");
         hook_index_lease6_select_ = HooksManager::registerHook("lease6_select");
-        hook_index_lease6_renew_   = HooksManager::registerHook("lease6_renew");
-        hook_index_lease6_rebind_   = HooksManager::registerHook("lease6_rebind");
+        hook_index_lease6_renew_  = HooksManager::registerHook("lease6_renew");
+        hook_index_lease6_rebind_ = HooksManager::registerHook("lease6_rebind");
+        hook_index_lease6_expire_ = HooksManager::registerHook("lease6_expire");
     }
 };
 
@@ -1291,8 +1297,6 @@ AllocEngine::reclaimExpiredLeases6(const size_t max_leases, const uint16_t timeo
 
     // Create stopwatch and automatically start it to measure the time
     // taken by the routine.
-    /// @todo Monitor time elapsed and return from the lease reclamation routine
-    /// if it hits the timeout value.
     util::Stopwatch stopwatch;
 
     LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
@@ -1300,23 +1304,50 @@ AllocEngine::reclaimExpiredLeases6(const size_t max_leases, const uint16_t timeo
     Lease6Collection leases;
     lease_mgr.getExpiredLeases6(leases, max_leases);
 
+    // Do not initialize the callout handle until we know if there are any
+    // lease6_expire callouts installed.
+    CalloutHandlePtr callout_handle;
+    if (!leases.empty() &&
+        HooksManager::getHooksManager().calloutsPresent(Hooks.hook_index_lease6_expire_)) {
+        callout_handle = HooksManager::createCalloutHandle();
+    }
+
+    size_t leases_processed = 0;
     BOOST_FOREACH(Lease6Ptr lease, leases) {
 
         try {
-            /// @todo execute a lease6_expire hook here.
+            // The skip flag indicates if the callouts have taken responsibility
+            // for reclaiming the lease. The callout will set this to true if
+            // it reclaims the lease itself. In this case the reclamation routine
+            // will not update DNS nor update the database.
+            bool skipped = false;
+            if (callout_handle) {
+                callout_handle->deleteAllArguments();
+                callout_handle->setArgument("lease6", lease);
+                callout_handle->setArgument("remove_lease", remove_lease);
+
+                HooksManager::callCallouts(Hooks.hook_index_lease6_expire_,
+                                           *callout_handle);
+
+                skipped = callout_handle->getSkip();
+            }
+
+            if (!skipped) {
+                // Generate removal name change request for D2, if required.
+                // This will return immediatelly if the DNS wasn't updated
+                // when the lease was created.
+                if (lease->duid_) {
+                    queueRemovalNameChangeRequest(lease, *(lease->duid_));
+                }
 
-            // Generate removal name change request for D2, if required.
-            // This will return immediatelly if the DNS wasn't updated
-            // when the lease was created.
-            if (lease->duid_) {
-                queueRemovalNameChangeRequest(lease, *(lease->duid_));
+                // Reclaim the lease - depending on the configuration, set the
+                // expired-reclaimed state or simply remove it.
+                reclaimLeaseInDatabase<Lease6Ptr>(lease, remove_lease,
+                                                  boost::bind(&LeaseMgr::updateLease6,
+                                                              &lease_mgr, _1));
             }
 
-            // Reclaim the lease - depending on the configuration, set the
-            // expired-reclaimed state or simply remove it.
-            reclaimLeaseInDatabase<Lease6Ptr>(lease, remove_lease,
-                                              boost::bind(&LeaseMgr::updateLease6,
-                                                          &lease_mgr, _1));
+            ++leases_processed;
 
             // Update statistics.
 
@@ -1352,6 +1383,16 @@ AllocEngine::reclaimExpiredLeases6(const size_t max_leases, const uint16_t timeo
                 .arg(lease->addr_.toText())
                 .arg(ex.what());
         }
+
+        // Check if we have hit the timeout for running reclamation routine and
+        // return if we have. We're checking it here, because we always want to
+        // allow reclaiming at least one lease.
+        if ((timeout > 0) && (stopwatch.getTotalMilliseconds() >= timeout)) {
+            LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
+                      ALLOC_ENGINE_V6_LEASES_RECLAMATION_TIMEOUT)
+                .arg(timeout);
+            break;
+        }
     }
 
     // Stop measuring the time.
@@ -1360,7 +1401,7 @@ AllocEngine::reclaimExpiredLeases6(const size_t max_leases, const uint16_t timeo
     // Mark completion of the lease reclamation routine and present some stats.
     LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
               ALLOC_ENGINE_V6_LEASES_RECLAMATION_COMPLETE)
-        .arg(leases.size())
+        .arg(leases_processed)
         .arg(stopwatch.logFormatTotalDuration());
 }
 
@@ -1375,8 +1416,6 @@ AllocEngine::reclaimExpiredLeases4(const size_t max_leases, const uint16_t timeo
 
     // Create stopwatch and automatically start it to measure the time
     // taken by the routine.
-    /// @todo Monitor time elapsed and return from the lease reclamation routine
-    /// if it hits the timeout value.
     util::Stopwatch stopwatch;
 
     LeaseMgr& lease_mgr = LeaseMgrFactory::instance();
@@ -1384,29 +1423,56 @@ AllocEngine::reclaimExpiredLeases4(const size_t max_leases, const uint16_t timeo
     Lease4Collection leases;
     lease_mgr.getExpiredLeases4(leases, max_leases);
 
+    // Do not initialize the callout handle until we know if there are any
+    // lease4_expire callouts installed.
+    CalloutHandlePtr callout_handle;
+    if (!leases.empty() &&
+        HooksManager::getHooksManager().calloutsPresent(Hooks.hook_index_lease4_expire_)) {
+        callout_handle = HooksManager::createCalloutHandle();
+    }
+
+    size_t leases_processed = 0;
     BOOST_FOREACH(Lease4Ptr lease, leases) {
 
         try {
-            /// @todo execute a lease4_expire hook here.
+            // The skip flag indicates if the callouts have taken responsibility
+            // for reclaiming the lease. The callout will set this to true if
+            // it reclaims the lease itself. In this case the reclamation routine
+            // will not update DNS nor update the database.
+            bool skipped = false;
+            if (callout_handle) {
+                callout_handle->deleteAllArguments();
+                callout_handle->setArgument("lease4", lease);
+                callout_handle->setArgument("remove_lease", remove_lease);
+
+                HooksManager::callCallouts(Hooks.hook_index_lease4_expire_,
+                                           *callout_handle);
+
+                skipped = callout_handle->getSkip();
+            }
 
-            // Generate removal name change request for D2, if required.
-            // This will return immediatelly if the DNS wasn't updated
-            // when the lease was created.
-            if (lease->client_id_) {
-                // Client id takes precedence over HW address.
-                queueRemovalNameChangeRequest(lease, lease->client_id_->getClientId());
+            if (!skipped) {
+                // Generate removal name change request for D2, if required.
+                // This will return immediatelly if the DNS wasn't updated
+                // when the lease was created.
+                if (lease->client_id_) {
+                    // Client id takes precedence over HW address.
+                    queueRemovalNameChangeRequest(lease, lease->client_id_->getClientId());
 
-            } else {
-                // Client id is not specified for the lease. Use HW address
-                // instead.
-                queueRemovalNameChangeRequest(lease, lease->hwaddr_);
+                } else {
+                    // Client id is not specified for the lease. Use HW address
+                    // instead.
+                    queueRemovalNameChangeRequest(lease, lease->hwaddr_);
+                }
+
+                // Reclaim the lease - depending on the configuration, set the
+                // expired-reclaimed state or simply remove it.
+                reclaimLeaseInDatabase<Lease4Ptr>(lease, remove_lease,
+                                                  boost::bind(&LeaseMgr::updateLease4,
+                                                              &lease_mgr, _1));
             }
 
-            // Reclaim the lease - depending on the configuration, set the
-            // expired-reclaimed state or simply remove it.
-            reclaimLeaseInDatabase<Lease4Ptr>(lease, remove_lease,
-                                              boost::bind(&LeaseMgr::updateLease4,
-                                                          &lease_mgr, _1));
+            ++leases_processed;
 
             // Update statistics.
 
@@ -1430,6 +1496,16 @@ AllocEngine::reclaimExpiredLeases4(const size_t max_leases, const uint16_t timeo
                 .arg(lease->addr_.toText())
                 .arg(ex.what());
         }
+
+        // Check if we have hit the timeout for running reclamation routine and
+        // return if we have. We're checking it here, because we always want to
+        // allow reclaiming at least one lease.
+        if ((timeout > 0) && (stopwatch.getTotalMilliseconds() >= timeout)) {
+            LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
+                      ALLOC_ENGINE_V6_LEASES_RECLAMATION_TIMEOUT)
+                .arg(timeout);
+            break;
+        }
     }
 
     // Stop measuring the time.
@@ -1438,7 +1514,7 @@ AllocEngine::reclaimExpiredLeases4(const size_t max_leases, const uint16_t timeo
     // Mark completion of the lease reclamation routine and present some stats.
     LOG_DEBUG(alloc_engine_logger, ALLOC_ENGINE_DBG_TRACE,
               ALLOC_ENGINE_V4_LEASES_RECLAMATION_COMPLETE)
-        .arg(leases.size())
+        .arg(leases_processed)
         .arg(stopwatch.logFormatTotalDuration());
 }
 

+ 4 - 4
src/lib/dhcpsrv/alloc_engine.h

@@ -480,7 +480,7 @@ public:
     ///   else (see host reservation)
     /// - client's leases does not match his reservations
     ///
-    /// This method will call  the lease6_renew callout.
+    /// This method will call the lease6_renew callout.
     ///
     /// @param ctx Message processing context. It holds various information
     /// extracted from the client's message and required to allocate a lease.
@@ -507,7 +507,7 @@ public:
     ///
     /// @param max_leases Maximum number of leases to be reclaimed.
     /// @param timeout Maximum amount of time that the reclaimation routine
-    /// may be processing expired leases, expressed in seconds.
+    /// may be processing expired leases, expressed in milliseconds.
     /// @param remove_lease A boolean value indicating if the lease should
     /// be removed when it is reclaimed (if true) or it should be left in the
     /// database in the "expired-reclaimed" state (if false).
@@ -522,7 +522,7 @@ public:
     ///
     /// This method is executed periodically to act upon expired leases. This
     /// includes for each lease:
-    /// - executing "lease_expire6" hook,
+    /// - executing "lease_expire4" hook,
     /// - removing DNS record for a lease,
     /// - reclaiming a lease in the database, i.e. setting its state to
     ///   "expired-reclaimed" or removing it from the lease databse,
@@ -530,7 +530,7 @@ public:
     ///
     /// @param max_leases Maximum number of leases to be reclaimed.
     /// @param timeout Maximum amount of time that the reclaimation routine
-    /// may be processing expired leases, expressed in seconds.
+    /// may be processing expired leases, expressed in milliseconds.
     /// @param remove_lease A boolean value indicating if the lease should
     /// be removed when it is reclaimed (if true) or it should be left in the
     /// database in the "expired-reclaimed" state (if false).

+ 16 - 0
src/lib/dhcpsrv/alloc_engine_messages.mes

@@ -82,6 +82,14 @@ reclamation of the expired leases. The maximum number of leases to
 be reclaimed and the timeout is included in the message. If any of
 these values is 0, it means "unlimited".
 
+% ALLOC_ENGINE_V4_LEASES_RECLAMATION_TIMEOUT timeout of %1 ms reached while reclaiming IPv4 leases
+This debug message is issued when the allocation engine hits the
+timeout for performing reclamation of the expired leases. The
+reclamation will now be interrupted and all leases which haven't
+been reclaimed, because of the timeout, will be reclaimed when the
+next scheduled reclamation is started. The argument is the timeout
+value expressed in milliseconds.
+
 % ALLOC_ENGINE_V4_OFFER_EXISTING_LEASE allocation engine will try to offer existing lease to the client %1
 This message is issued when the allocation engine determines that
 the client has a lease in the lease database, it doesn't have
@@ -300,6 +308,14 @@ reclamation of the expired leases. The maximum number of leases to
 be reclaimed and the timeout is included in the message. If any of
 these values is 0, it means "unlimited".
 
+% ALLOC_ENGINE_V6_LEASES_RECLAMATION_TIMEOUT timeout of %1 ms reached while reclaiming IPv6 leases
+This debug message is issued when the allocation engine hits the
+timeout for performing reclamation of the expired leases. The
+reclamation will now be interrupted and all leases which haven't
+been reclaimed, because of the timeout, will be reclaimed when the
+next scheduled reclamation is started. The argument is the timeout
+value expressed in milliseconds.
+
 % ALLOC_ENGINE_V6_RENEW_HR allocating leases reserved for the client %1 as a result of Renew
 This debug message is issued when the allocation engine tries to
 allocate reserved leases for the client sending a Renew message.

+ 249 - 6
src/lib/dhcpsrv/tests/alloc_engine_expiration_unittest.cc

@@ -18,6 +18,7 @@
 #include <dhcp_ddns/ncr_msg.h>
 #include <dhcpsrv/tests/alloc_engine_utils.h>
 #include <dhcpsrv/tests/test_utils.h>
+#include <hooks/hooks_manager.h>
 #include <stats/stats_mgr.h>
 #include <gtest/gtest.h>
 #include <boost/bind.hpp>
@@ -26,6 +27,7 @@
 #include <iomanip>
 #include <sstream>
 #include <time.h>
+#include <unistd.h>
 #include <vector>
 
 using namespace std;
@@ -34,6 +36,7 @@ using namespace isc::asiolink;
 using namespace isc::dhcp;
 using namespace isc::dhcp::test;
 using namespace isc::dhcp_ddns;
+using namespace isc::hooks;
 using namespace isc::stats;
 
 namespace {
@@ -88,6 +91,12 @@ struct UpperBound {
     size_t upper_bound_;
 };
 
+/// @brief List holding addresses for executed callouts.
+std::list<IOAddress> callouts_;
+
+/// @brief Callout argument name for expired lease.
+std::string callout_argument_name("lease4");
+
 /// @brief Base test fixture class for the lease reclamation routines in the
 /// @c AllocEngine.
 ///
@@ -150,11 +159,6 @@ struct UpperBound {
 /// See @c ExpirationAllocEngineTest::testLeases for further details.
 ///
 /// @todo These tests should be extended to cover the following cases:
-/// - timeout value in the lease reclamation routines - the most reliable
-///   way to test it will be by attaching a lease4_expire/lease6_expire
-///   hooks which would block for a specific period of time. This will
-///   allow for calculating the approximate number of reclaimed leases
-///   within a given timeout. See ticket #3972.
 /// - declined leases - declined leases expire and should be removed
 ///   from the lease database by the lease reclamation routine. See
 ///   ticket #3976.
@@ -211,6 +215,9 @@ public:
 
         // Kill lease manager.
         LeaseMgrFactory::destroy();
+
+        // Remove callouts executed.
+        callouts_.clear();
     }
 
     /// @brief Starts D2 client.
@@ -452,6 +459,75 @@ public:
         return (true);
     }
 
+    /// @brief Lease algorithm checking if callout has been executed for
+    /// the expired lease.
+    ///
+    /// @param lease Pointer to lease.
+    /// @return true if callout has been executed for the lease.
+    static bool leaseCalloutExecuted(const LeasePtrType& lease) {
+        return (std::find(callouts_.begin(), callouts_.end(), lease->addr_) !=
+                callouts_.end());
+    }
+
+    /// @brief Lease algorithm checking if callout hasn't been executed for
+    /// the expired lease.
+    ///
+    /// @param lease Pointer to lease.
+    /// @return true if callout hasn't been executed for the lease.
+    static bool leaseCalloutNotExecuted(const LeasePtrType& lease) {
+        return (!leaseCalloutExecuted(lease));
+    }
+
+    /// @brief Implements "lease{4,6}_expire" callout.
+    ///
+    /// @param callout_handle Callout handle.
+    /// @return Zero.
+    static int leaseExpireCallout(CalloutHandle& callout_handle) {
+        LeasePtrType lease;
+        callout_handle.getArgument(callout_argument_name, lease);
+        bool remove_lease = true;
+        callout_handle.getArgument("remove_lease", remove_lease);
+
+        // Check if the remove_lease is set to false and assume that the callout
+        // has been successfully executed if it is. This is mainly to test
+        // that the lease reclamation routine sets this value at all.
+        if (!remove_lease) {
+            callouts_.push_back(lease->addr_);
+        }
+
+        return (0);
+    }
+
+    /// @brief Implements "lease{4,6}_expire callout returning skip flag.
+    ///
+    /// @param callout_handle Callout handle.
+    /// @return Zero.
+    static int leaseExpireWithSkipCallout(CalloutHandle& callout_handle) {
+        leaseExpireCallout(callout_handle);
+        callout_handle.setSkip(true);
+
+        return (0);
+    }
+
+    /// @brief Implements "lease{4,6}_expire callout, which lasts at least
+    /// 2ms.
+    ///
+    /// This callout is useful to test scenarios where the reclamation of the
+    /// lease needs to take a known amount of time. If the callout is installed
+    /// it will take at least 2ms for each lease. It is then possible to calculate
+    /// the approximate time that the reclamation of all leases would take and
+    /// test that the timeouts for the leases' reclamation work as expected.
+    ///
+    /// @param callout_handle Callout handle.
+    /// @return Zero.
+    static int leaseExpireWithDelayCallout(CalloutHandle& callout_handle) {
+        leaseExpireCallout(callout_handle);
+        // Delay the return from the callout by 2ms.
+        usleep(2000);
+
+        return (0);
+    }
+
     /// @brief Returns removal name change request from the D2 client queue.
     ///
     /// @param lease Pointer to the lease to be matched with NCR.
@@ -734,12 +810,113 @@ public:
         EXPECT_TRUE(testLeases(&dnsUpdateGeneratedForLease, &oddLeaseIndex));
     }
 
+    /// @brief This test verfies that callouts are executed for each expired
+    /// lease when installed.
+    void testReclaimExpiredLeasesHooks() {
+        for (int i = 0; i < TEST_LEASES_NUM; ++i) {
+            if (evenLeaseIndex(i)) {
+                expire(i, 1000 - i);
+            }
+        }
+
+        vector<string> libraries; // no libraries at this time
+        HooksManager::loadLibraries(libraries);
+
+        // Install a callout: lease4_expire or lease6_expire.
+        std::ostringstream callout_name;
+        callout_name << callout_argument_name << "_expire";
+        EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        callout_name.str(), leaseExpireCallout));
+
+        ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+        // Callouts should be executed for leases with even indexes and these
+        // leases should be reclaimed.
+        EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex));
+        EXPECT_TRUE(testLeases(&leaseReclaimed, &evenLeaseIndex));
+        // Callouts should not be executed for leases with odd indexes and these
+        // leases should not be reclaimed.
+        EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex));
+        EXPECT_TRUE(testLeases(&leaseNotReclaimed, &oddLeaseIndex));
+    }
+
+    /// @brief This test verfies that callouts are executed for each expired
+    /// lease and that the lease is not reclaimed when skip flag is set.
+    void testReclaimExpiredLeasesHooksWithSkip() {
+        for (int i = 0; i < TEST_LEASES_NUM; ++i) {
+            if (evenLeaseIndex(i)) {
+                expire(i, 1000 - i);
+            }
+        }
+
+        vector<string> libraries; // no libraries at this time
+        HooksManager::loadLibraries(libraries);
+
+        // Install a callout: lease4_expire or lease6_expire.
+        std::ostringstream callout_name;
+        callout_name << callout_argument_name << "_expire";
+        EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        callout_name.str(), leaseExpireWithSkipCallout));
+
+        ASSERT_NO_THROW(reclaimExpiredLeases(0, 0, false));
+
+        // Callouts should have been executed for leases with even indexes.
+        EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &evenLeaseIndex));
+        // Callouts should not be executed for leases with odd indexes.
+        EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &oddLeaseIndex));
+        // Leases shouldn't be reclaimed because the callout sets the
+        // skip flag for each of them.
+        EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes));
+    }
+
+    /// @brief This test verifies that it is possible to set the timeout for
+    /// the execution of the lease reclamation routine.
+    void testReclaimExpiredLeasesTimeout(const uint16_t timeout) {
+        // Leases are segregated from the most expired to the least expired.
+        for (int i = 0; i < TEST_LEASES_NUM; ++i) {
+            expire(i, 2000 - i);
+        }
+
+        vector<string> libraries;
+        HooksManager::loadLibraries(libraries);
+
+        // Install a callout: lease4_expire or lease6_expire. Each callout
+        // takes at least 2ms to run (it uses usleep).
+        std::ostringstream callout_name;
+        callout_name << callout_argument_name << "_expire";
+        EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+                        callout_name.str(), leaseExpireWithDelayCallout));
+
+        // Reclaim leases with timeout.
+        ASSERT_NO_THROW(reclaimExpiredLeases(0, timeout, false));
+
+        // We reclaimed at most (timeout / 2ms) leases.
+        const uint16_t theoretical_reclaimed = static_cast<uint16_t>(timeout / 2);
+
+        // The actual number of leases reclaimed is likely to be lower than
+        // the theoretical number. For low theoretical number the adjusted
+        // number is always 1. For higher number, it will be 10 less than the
+        // theoretical number.
+        const uint16_t adjusted_reclaimed = (theoretical_reclaimed > 10 ?
+                                             theoretical_reclaimed - 10 : 1);
+
+        EXPECT_TRUE(testLeases(&leaseCalloutExecuted, &allLeaseIndexes,
+                               LowerBound(0), UpperBound(adjusted_reclaimed)));
+        EXPECT_TRUE(testLeases(&leaseReclaimed, &allLeaseIndexes,
+                               LowerBound(0), UpperBound(adjusted_reclaimed)));
+        EXPECT_TRUE(testLeases(&leaseCalloutNotExecuted, &allLeaseIndexes,
+                               LowerBound(theoretical_reclaimed + 1),
+                               UpperBound(TEST_LEASES_NUM)));
+        EXPECT_TRUE(testLeases(&leaseNotReclaimed, &allLeaseIndexes,
+                               LowerBound(theoretical_reclaimed + 1),
+                               UpperBound(TEST_LEASES_NUM)));
+    }
+
     /// @brief Collection of leases created at construction time.
     std::vector<LeasePtrType> leases_;
 
     /// @brief Allocation engine instance used for tests.
     AllocEnginePtr engine_;
-
 };
 
 /// @brief Specialization of the @c ExpirationAllocEngineTest class to test
@@ -811,6 +988,7 @@ public:
 ExpirationAllocEngine6Test::ExpirationAllocEngine6Test()
     : ExpirationAllocEngineTest<Lease6Ptr>("type=memfile universe=6 persist=false") {
     createLeases();
+    callout_argument_name = "lease6";
 }
 
 void
@@ -967,6 +1145,38 @@ TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesStats) {
     testReclaimExpiredLeasesStats();
 }
 
+// This test verifies that callouts are executed for each expired lease.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooks) {
+    testReclaimExpiredLeasesHooks();
+}
+
+// This test verifies that callouts are executed for each expired lease
+// and that the lease is not reclaimed when the skip flag is set.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesHooksWithSkip) {
+    testReclaimExpiredLeasesHooksWithSkip();
+}
+
+// This test verifies that it is possible to set the timeout for the
+// execution of the lease reclamation routine.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesTimeout) {
+    // This test needs at least 40 leases to make sense.
+    BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40);
+    // Run with timeout of 60ms.
+    testReclaimExpiredLeasesTimeout(60);
+}
+
+// This test verifies that at least one lease is reclaimed if the timeout
+// for the lease reclamation routine is shorter than the time needed for
+// the reclamation of a single lease. This prevents the situation when
+// very short timeout (perhaps misconfigured) effectively precludes leases
+// reclamation.
+TEST_F(ExpirationAllocEngine6Test, reclaimExpiredLeasesShortTimeout) {
+    // We will most likely reclaim just one lease, so 5 is more than enough.
+    BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5);
+    // Reclaim leases with the 1ms timeout.
+    testReclaimExpiredLeasesTimeout(1);
+}
+
 // *******************************************************
 //
 // DHCPv4 lease reclamation routine tests start here!
@@ -1052,6 +1262,7 @@ public:
 ExpirationAllocEngine4Test::ExpirationAllocEngine4Test()
     : ExpirationAllocEngineTest<Lease4Ptr>("type=memfile universe=4 persist=false") {
     createLeases();
+    callout_argument_name = "lease4";
 }
 
 void
@@ -1298,4 +1509,36 @@ TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesStats) {
     testReclaimExpiredLeasesStats();
 }
 
+// This test verifies that callouts are executed for each expired lease.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooks) {
+    testReclaimExpiredLeasesHooks();
+}
+
+// This test verifies that callouts are executed for each expired lease
+// and that the lease is not reclaimed when the skip flag is set.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesHooksWithSkip) {
+    testReclaimExpiredLeasesHooksWithSkip();
+}
+
+// This test verifies that it is possible to set the timeout for the
+// execution of the lease reclamation routine.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesTimeout) {
+    // This test needs at least 40 leases to make sense.
+    BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 40);
+    // Run with timeout of 60ms.
+    testReclaimExpiredLeasesTimeout(60);
+}
+
+// This test verifies that at least one lease is reclaimed if the timeout
+// for the lease reclamation routine is shorter than the time needed for
+// the reclamation of a single lease. This prevents the situation when
+// very short timeout (perhaps misconfigured) effectively precludes leases
+// reclamation.
+TEST_F(ExpirationAllocEngine4Test, reclaimExpiredLeasesShortTimeout) {
+    // We will most likely reclaim just one lease, so 5 is more than enough.
+    BOOST_STATIC_ASSERT(TEST_LEASES_NUM >= 5);
+    // Reclaim leases with the 1ms timeout.
+    testReclaimExpiredLeasesTimeout(1);
+}
+
 }; // end of anonymous namespace