Repository: eclipse/leshan
Pull request #713 (2019-07-04 14:35:06)

Make bootstrap server more robust and customizable.
1. Add more callback on BootstrapSessionManager for better monitoring.
2. Add a way to customize bootstrapHandler behavior on error.  
3. Improve BootstrapHandler to make it able to avoid to start 2 bootstrap session at the same time for the same device.  
4. Logs enhancement

sbernard31: In `DefaultBootstrapHandler`, I asked myself if the executor is really needed ? I would go for remove it. :thinking: 

WDYT ? 
sbernard31: Any thought about the executor ?

Patch:
diff --git a/leshan-bsserver-demo/logback-config.xml b/leshan-bsserver-demo/logback-config.xml
index cc51f900..1f4aa778 100644
--- a/leshan-bsserver-demo/logback-config.xml
+++ b/leshan-bsserver-demo/logback-config.xml
@@ -27,6 +27,6 @@ Contributors:
 
 	<logger name="org.eclipse.leshan" level="INFO" />
 	<logger name="org.eclipse.leshan.server.security.SecurityCheck" level="DEBUG" />
-	<logger name="org.eclipse.leshan.server.bootstrap.BootstrapHandler" level="DEBUG" />
+	<logger name="org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler" level="DEBUG" />
 	<logger name="org.eclipse.leshan.core.model.LwM2mModel" level="DEBUG" />
 </configuration>
\ No newline at end of file
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/BootstrapResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/BootstrapResource.java
index 2cb987b6..f674ca53 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/BootstrapResource.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/BootstrapResource.java
@@ -28,6 +28,7 @@ import org.eclipse.leshan.core.californium.EndpointContextUtil;
 import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.core.response.BootstrapResponse;
+import org.eclipse.leshan.core.response.SendableResponse;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -79,11 +80,14 @@ public class BootstrapResource extends CoapResource {
         Identity clientIdentity = EndpointContextUtil.extractIdentity(request.getSourceContext());
 
         // handle bootstrap request
-        BootstrapResponse response = bootstrapHandler.bootstrap(clientIdentity, new BootstrapRequest(endpoint));
+        SendableResponse<BootstrapResponse> sendableResponse = bootstrapHandler.bootstrap(clientIdentity,
+                new BootstrapRequest(endpoint));
+        BootstrapResponse response = sendableResponse.getResponse();
         if (response.isSuccess()) {
             exchange.respond(toCoapResponseCode(response.getCode()));
         } else {
             exchange.respond(toCoapResponseCode(response.getCode()), response.getErrorMessage());
         }
+        sendableResponse.sent();
     }
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java
index 094bba51..01e2b032 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java
@@ -23,12 +23,16 @@ public enum BootstrapFailureCause {
      * The device presented wrong credentials
      */
     UNAUTHORIZED,
+    /**
+     * A bootstrap session is already started for the given device.
+     */
+    ALREADY_STARTED,
     /**
      * The Bootstrap Server could not find a configuration to send to the device
      */
     NO_BOOTSTRAP_CONFIG,
     /**
-     * The "/" object could not be deleted on the device
+     * A delete request failed
      */
     DELETE_FAILED,
     /**
@@ -44,11 +48,11 @@ public enum BootstrapFailureCause {
      */
     WRITE_SECURITY_FAILED,
     /**
-     * 'Bootstrap Finish' message count not be sent to the device
+     * 'Bootstrap Finish' failed
      */
-    SEND_FINISH_FAILED,
+    FINISH_FAILED,
     /**
-     * The device responded to 'Bootstrap Finish' with an error code
+     * An unexpected error occured
      */
-    FINISHED_WITH_ERROR
+    INTERNAL_SERVER_ERROR,
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
index 2c7d065e..4c834548 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java
@@ -18,8 +18,9 @@ package org.eclipse.leshan.server.bootstrap;
 import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.core.response.BootstrapResponse;
+import org.eclipse.leshan.core.response.SendableResponse;
 
 public interface BootstrapHandler {
 
-    BootstrapResponse bootstrap(Identity sender, BootstrapRequest request);
+    SendableResponse<BootstrapResponse> bootstrap(Identity sender, BootstrapRequest request);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSession.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSession.java
index 91e65feb..9921690a 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSession.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSession.java
@@ -46,4 +46,10 @@ public interface BootstrapSession {
      */
     ContentFormat getContentFormat();
 
+    /**
+     * @return the create time in milliseconds
+     * @see System#currentTimeMillis()
+     */
+    long getCreationTime();
+
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionManager.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionManager.java
index cdfa113d..42cb509e 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionManager.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionManager.java
@@ -15,13 +15,13 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.bootstrap;
 
-import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.request.LwM2mRequest;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.server.impl.DefaultBootstrapSessionManager;
 
 /**
- * Manages boundaries of bootstrap process.
+ * Manages life cycle of a bootstrap process.
  * <p>
  * This class is responsible to accept or refuse to start new {@link BootstrapSession}. The session also contain the
  * ContentFormat which will be used to send Bootstrap Write Request.
@@ -31,6 +31,29 @@ import org.eclipse.leshan.server.impl.DefaultBootstrapSessionManager;
  */
 public interface BootstrapSessionManager {
 
+    public enum BootstrapPolicy {
+        /**
+         * Continue bootstrap session ignoring failure.
+         */
+        CONTINUE,
+        /**
+         * Try to resend the failed request.
+         */
+        RETRY,
+        /**
+         * Retry the bootstrap session from the beginning generally.
+         */
+        RETRYALL,
+        /**
+         * Stop to try to write bootstrap config and directly send a "Bootstrap Finished" request.
+         */
+        SEND_FINISHED,
+        /**
+         * Stop the bootstrap session. The session is considered as failure.
+         */
+        STOP
+    }
+
     /**
      * Starts a bootstrapping session for an endpoint. In particular, this is responsible for authorizing the endpoint
      * if applicable
@@ -42,6 +65,38 @@ public interface BootstrapSessionManager {
      */
     public BootstrapSession begin(String endpoint, Identity clientIdentity);
 
+    /**
+     * Called when we receive a successful response to a request.
+     * 
+     * @param bsSession the bootstrap session concerned.
+     * @param request The request for which we get a successfull response.
+     */
+    public void onResponseSuccess(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request);
+
+    /**
+     * Called when we receive a error response to a request.
+     * 
+     * @param bsSession the bootstrap session concerned.
+     * @param request The request for which we get a error response.
+     * @param response The response received.
+     * 
+     * @return a {@link BootstrapPolicy} given the way to continue the bootstrap session.
+     */
+    public BootstrapPolicy onResponseError(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response);
+
+    /**
+     * Called when a request failed to be sent.
+     * 
+     * @param bsSession the bootstrap session concerned.
+     * @param request The request which failed to be sent.
+     * @param exception The cause of the failure. Can be null.
+     * 
+     * @return a {@link BootstrapPolicy} given the way to continue the bootstrap session.
+     */
+    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+            Throwable cause);
+
     /**
      * Performs any housekeeping related to the successful ending of a Bootstrapping session.
      * 
@@ -54,8 +109,6 @@ public interface BootstrapSessionManager {
      * 
      * @param bsSession the bootstrap session which failed.
      * @param cause why the bootstrap failed.
-     * @param request last request sent to the device. Can be null.
      */
-    public void failed(BootstrapSession bsSession, BootstrapFailureCause cause,
-            DownlinkRequest<? extends LwM2mResponse> request);
+    public void failed(BootstrapSession bsSession, BootstrapFailureCause cause);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
index a1761eb3..0012ae8d 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
@@ -19,8 +19,7 @@ import static org.eclipse.leshan.server.bootstrap.BootstrapFailureCause.*;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
@@ -37,9 +36,11 @@ import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.core.response.SendableResponse;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager.BootstrapPolicy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,88 +55,131 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
     // send a Confirmable message to the time when an acknowledgement is no longer expected.
-    protected static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms
+    public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms
 
-    protected final Executor e;
+    public static final long DEFAULT_LIFETIME = 15 * 60 * 1000l; // 15 minutes
 
     protected final BootstrapConfigStore store;
+
     protected final LwM2mBootstrapRequestSender sender;
-    protected final BootstrapSessionManager sessionManager;
     protected final long requestTimeout;
 
-    public DefaultBootstrapHandler(BootstrapConfigStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager) {
-        this(store, sender, sessionManager, Executors.newFixedThreadPool(5), DEFAULT_TIMEOUT);
-    }
+    // Session should be removed from onGoingSession map when it failed or succeed.
+    // But in case where we are not able to detect a failure we define a maximum lifetime to avoid to completely block a
+    // device
+    protected final long sessionLifeTime;
+    protected final ConcurrentHashMap<String, BootstrapSession> onGoingSession = new ConcurrentHashMap<>();
+    protected final BootstrapSessionManager sessionManager;
 
     public DefaultBootstrapHandler(BootstrapConfigStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager, Executor executor) {
-        this(store, sender, sessionManager, executor, DEFAULT_TIMEOUT);
+            BootstrapSessionManager sessionManager) {
+        this(store, sender, sessionManager, DEFAULT_TIMEOUT, DEFAULT_LIFETIME);
     }
 
     public DefaultBootstrapHandler(BootstrapConfigStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager, Executor executor, long requestTimeout) {
+            BootstrapSessionManager sessionManager, long requestTimeout, long sessionLifetime) {
         this.store = store;
         this.sender = sender;
         this.sessionManager = sessionManager;
-        this.e = executor;
         this.requestTimeout = requestTimeout;
+        this.sessionLifeTime = sessionLifetime;
     }
 
     @Override
-    public BootstrapResponse bootstrap(Identity sender, BootstrapRequest request) {
+    public SendableResponse<BootstrapResponse> bootstrap(Identity sender, BootstrapRequest request) {
         String endpoint = request.getEndpointName();
 
         // Start session, checking the BS credentials
         final BootstrapSession session = this.sessionManager.begin(endpoint, sender);
 
         if (!session.isAuthorized()) {
-            this.sessionManager.failed(session, UNAUTHORIZED, null);
-            return BootstrapResponse.badRequest("Unauthorized");
-        }
-
-        // Get the desired bootstrap config for the endpoint
-        final BootstrapConfig cfg = store.get(endpoint, sender);
-        if (cfg == null) {
-            LOG.debug("No bootstrap config for {}/{}", endpoint, sender);
-            this.sessionManager.failed(session, NO_BOOTSTRAP_CONFIG, null);
-            return BootstrapResponse.badRequest("no bootstrap config");
+            sessionManager.failed(session, UNAUTHORIZED);
+            return new SendableResponse<>(BootstrapResponse.badRequest("Unauthorized"));
         }
 
-        // Start the bootstrap session
-        e.execute(new Runnable() {
-            @Override
-            public void run() {
-                startBootstrap(session, cfg);
+        // check if there is not an ongoing session.
+        BootstrapSession oldSession;
+        do {
+            oldSession = onGoingSession.putIfAbsent(endpoint, session);
+            if (oldSession != null) {
+                if (System.currentTimeMillis() - oldSession.getCreationTime() >= sessionLifeTime) {
+                    onGoingSession.remove(endpoint, oldSession);
+                    if (LOG.isWarnEnabled())
+                        LOG.warn("{} expired at {}.", oldSession, System.currentTimeMillis());
+                } else {
+                    // Do not start the session if there is already a started one
+                    sessionManager.failed(session, ALREADY_STARTED);
+                    return new SendableResponse<>(BootstrapResponse.badRequest("session already started"));
+                }
+            }
+        } while (oldSession != null);
+
+        try {
+            // Get the desired bootstrap config for the endpoint
+            final BootstrapConfig cfg = store.get(endpoint, sender);
+            if (cfg == null) {
+                LOG.debug("No bootstrap config for {}", session);
+                stopSession(session, NO_BOOTSTRAP_CONFIG);
+                return new SendableResponse<>(BootstrapResponse.badRequest("no bootstrap config"));
             }
-        });
 
-        return BootstrapResponse.success();
+            // Start bootstrap once response is sent.
+            Runnable onSent = new Runnable() {
+                @Override
+                public void run() {
+                    startBootstrap(session, cfg);
+                }
+            };
+            return new SendableResponse<>(BootstrapResponse.success(), onSent);
+
+        } catch (RuntimeException e) {
+            LOG.warn("Unexpected error at bootstrap start-up for {}", session, e);
+            stopSession(session, INTERNAL_SERVER_ERROR);
+            return new SendableResponse<>(BootstrapResponse.internalServerError(e.getMessage()));
+        }
     }
 
     protected void startBootstrap(BootstrapSession session, BootstrapConfig cfg) {
         delete(session, cfg, new ArrayList<>(cfg.toDelete));
     }
 
+    protected void stopSession(BootstrapSession session, BootstrapFailureCause cause) {
+        if (!onGoingSession.remove(session.getEndpoint(), session)) {
+            LOG.warn("{} was already removed", session);
+        }
+        // if there is no cause of failure, this is a success
+        if (cause == null) {
+            sessionManager.end(session);
+        } else {
+            sessionManager.failed(session, cause);
+        }
+    }
+
     protected void delete(final BootstrapSession session, final BootstrapConfig cfg, final List<String> pathToDelete) {
         if (!pathToDelete.isEmpty()) {
             // get next Security configuration
-            String path = pathToDelete.remove(0);
+            String path = pathToDelete.get(0);
 
             final BootstrapDeleteRequest deleteRequest = new BootstrapDeleteRequest(path);
-            send(session, deleteRequest, new ResponseCallback<BootstrapDeleteResponse>() {
+            send(session, deleteRequest, new SafeResponseCallback<BootstrapDeleteResponse>(session) {
                 @Override
-                public void onResponse(BootstrapDeleteResponse response) {
-                    LOG.trace("Bootstrap delete {} return code {}", session.getEndpoint(), response.getCode());
-
-                    delete(session, cfg, pathToDelete);
+                public void safeOnResponse(BootstrapDeleteResponse response) {
+                    if (response.isSuccess()) {
+                        LOG.trace("{} receives {} for {}", session, response, deleteRequest);
+                        sessionManager.onResponseSuccess(session, deleteRequest);
+                        afterDelete(session, cfg, pathToDelete, BootstrapPolicy.CONTINUE);
+                    } else {
+                        LOG.debug("{} receives {} for {}", session, response, deleteRequest);
+                        BootstrapPolicy policy = sessionManager.onResponseError(session, deleteRequest, response);
+                        afterDelete(session, cfg, pathToDelete, policy);
+                    }
                 }
-            }, new ErrorCallback() {
+            }, new SafeErrorCallback(session) {
                 @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format("Error during bootstrap delete '/' to foreign peer [ep=%s,id=%s]",
-                            session.getEndpoint(), session.getIdentity()), e);
-                    sessionManager.failed(session, DELETE_FAILED, deleteRequest);
+                public void safeOnError(Exception e) {
+                    LOG.debug("Error for {} while sending {} ", session, deleteRequest, e);
+                    BootstrapPolicy policy = sessionManager.onRequestFailure(session, deleteRequest, e);
+                    afterDelete(session, cfg, pathToDelete, policy);
                 }
             });
         } else {
@@ -145,11 +189,35 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         }
     }
 
+    protected void afterDelete(BootstrapSession session, BootstrapConfig cfg, List<String> pathToDelete,
+            BootstrapPolicy policy) {
+        switch (policy) {
+        case CONTINUE:
+            pathToDelete.remove(0);
+            delete(session, cfg, pathToDelete);
+            break;
+        case RETRY:
+            delete(session, cfg, pathToDelete);
+            break;
+        case RETRYALL:
+            startBootstrap(session, cfg);
+            break;
+        case SEND_FINISHED:
+            bootstrapFinished(session, cfg);
+            break;
+        case STOP:
+            stopSession(session, DELETE_FAILED);
+            break;
+        default:
+            throw new IllegalStateException("unknown policy :" + policy);
+        }
+    }
+
     protected void writeSecurities(final BootstrapSession session, final BootstrapConfig cfg,
             final List<Integer> securityInstancesToWrite) {
         if (!securityInstancesToWrite.isEmpty()) {
             // get next Security configuration
-            Integer key = securityInstancesToWrite.remove(0);
+            Integer key = securityInstancesToWrite.get(0);
             ServerSecurity securityConfig = cfg.security.get(key);
 
             // create write request from it
@@ -159,20 +227,26 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
                     session.getContentFormat());
 
             // sent it
-            send(session, writeBootstrapRequest, new ResponseCallback<BootstrapWriteResponse>() {
+            send(session, writeBootstrapRequest, new SafeResponseCallback<BootstrapWriteResponse>(session) {
                 @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until securityInstancesToWrite is empty
-                    writeSecurities(session, cfg, securityInstancesToWrite);
+                public void safeOnResponse(BootstrapWriteResponse response) {
+                    if (response.isSuccess()) {
+                        LOG.trace("{} receives {} for {}", session, response, writeBootstrapRequest);
+                        sessionManager.onResponseSuccess(session, writeBootstrapRequest);
+                        afterWriteSecurities(session, cfg, securityInstancesToWrite, BootstrapPolicy.CONTINUE);
+                    } else {
+                        LOG.debug("{} receives {} for {}", session, response, writeBootstrapRequest);
+                        BootstrapPolicy policy = sessionManager.onResponseError(session, writeBootstrapRequest,
+                                response);
+                        afterWriteSecurities(session, cfg, securityInstancesToWrite, policy);
+                    }
                 }
-            }, new ErrorCallback() {
+            }, new SafeErrorCallback(session) {
                 @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format(
-                            "Error during bootstrap write of security instance %s to foreign peer [ep=%s,id=%s]",
-                            securityInstance, session.getEndpoint(), session.getIdentity()), e);
-                    sessionManager.failed(session, WRITE_SECURITY_FAILED, writeBootstrapRequest);
+                public void safeOnError(Exception e) {
+                    LOG.debug("Error for {} while sending {} ", session, writeBootstrapRequest, e);
+                    BootstrapPolicy policy = sessionManager.onRequestFailure(session, writeBootstrapRequest, e);
+                    afterWriteSecurities(session, cfg, securityInstancesToWrite, policy);
                 }
             });
         } else {
@@ -182,11 +256,35 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         }
     }
 
+    protected void afterWriteSecurities(BootstrapSession session, BootstrapConfig cfg,
+            List<Integer> securityInstancesToWrite, BootstrapPolicy policy) {
+        switch (policy) {
+        case CONTINUE:
+            securityInstancesToWrite.remove(0);
+            writeSecurities(session, cfg, securityInstancesToWrite);
+            break;
+        case RETRY:
+            writeSecurities(session, cfg, securityInstancesToWrite);
+            break;
+        case RETRYALL:
+            startBootstrap(session, cfg);
+            break;
+        case SEND_FINISHED:
+            bootstrapFinished(session, cfg);
+            break;
+        case STOP:
+            stopSession(session, WRITE_SECURITY_FAILED);
+            break;
+        default:
+            throw new IllegalStateException("unknown policy :" + policy);
+        }
+    }
+
     protected void writeServers(final BootstrapSession session, final BootstrapConfig cfg,
             final List<Integer> serverInstancesToWrite) {
         if (!serverInstancesToWrite.isEmpty()) {
             // get next Server configuration
-            Integer key = serverInstancesToWrite.remove(0);
+            Integer key = serverInstancesToWrite.get(0);
             ServerConfig serverConfig = cfg.servers.get(key);
 
             // create write request from it
@@ -196,20 +294,25 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
                     session.getContentFormat());
 
             // sent it
-            send(session, writeServerRequest, new ResponseCallback<BootstrapWriteResponse>() {
+            send(session, writeServerRequest, new SafeResponseCallback<BootstrapWriteResponse>(session) {
                 @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until serverInstancesToWrite is empty
-                    writeServers(session, cfg, serverInstancesToWrite);
+                public void safeOnResponse(BootstrapWriteResponse response) {
+                    if (response.isSuccess()) {
+                        LOG.trace("{} receives {} for {}", session, response, writeServerRequest);
+                        sessionManager.onResponseSuccess(session, writeServerRequest);
+                        afterWriteServers(session, cfg, serverInstancesToWrite, BootstrapPolicy.CONTINUE);
+                    } else {
+                        LOG.debug("{} receives {} for {}", session, response, writeServerRequest);
+                        BootstrapPolicy policy = sessionManager.onResponseError(session, writeServerRequest, response);
+                        afterWriteServers(session, cfg, serverInstancesToWrite, policy);
+                    }
                 }
-            }, new ErrorCallback() {
+            }, new SafeErrorCallback(session) {
                 @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format(
-                            "Error during bootstrap write of server instance %s to foreign peer [ep=%s,id=%s]",
-                            serverInstance, session.getEndpoint(), session.getIdentity()), e);
-                    sessionManager.failed(session, WRITE_SERVER_FAILED, writeServerRequest);
+                public void safeOnError(Exception e) {
+                    LOG.debug("Error for {} while sending {} ", session, writeServerRequest, e);
+                    BootstrapPolicy policy = sessionManager.onRequestFailure(session, writeServerRequest, e);
+                    afterWriteServers(session, cfg, serverInstancesToWrite, policy);
                 }
             });
         } else {
@@ -219,11 +322,35 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         }
     }
 
+    protected void afterWriteServers(BootstrapSession session, BootstrapConfig cfg,
+            List<Integer> serverInstancesToWrite, BootstrapPolicy policy) {
+        switch (policy) {
+        case CONTINUE:
+            serverInstancesToWrite.remove(0);
+            writeServers(session, cfg, serverInstancesToWrite);
+            break;
+        case RETRY:
+            writeServers(session, cfg, serverInstancesToWrite);
+            break;
+        case RETRYALL:
+            startBootstrap(session, cfg);
+            break;
+        case SEND_FINISHED:
+            bootstrapFinished(session, cfg);
+            break;
+        case STOP:
+            stopSession(session, WRITE_SERVER_FAILED);
+            break;
+        default:
+            throw new IllegalStateException("unknown policy :" + policy);
+        }
+    }
+
     protected void writedAcls(final BootstrapSession session, final BootstrapConfig cfg,
             final List<Integer> aclInstancesToWrite) {
         if (!aclInstancesToWrite.isEmpty()) {
             // get next ACL configuration
-            Integer key = aclInstancesToWrite.remove(0);
+            Integer key = aclInstancesToWrite.get(0);
             ACLConfig aclConfig = cfg.acls.get(key);
 
             // create write request from it
@@ -233,20 +360,25 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
                     session.getContentFormat());
 
             // sent it
-            send(session, writeACLRequest, new ResponseCallback<BootstrapWriteResponse>() {
+            send(session, writeACLRequest, new SafeResponseCallback<BootstrapWriteResponse>(session) {
                 @Override
-                public void onResponse(BootstrapWriteResponse response) {
-                    LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode());
-                    // recursive call until aclInstancesToWrite is empty
-                    writedAcls(session, cfg, aclInstancesToWrite);
+                public void safeOnResponse(BootstrapWriteResponse response) {
+                    if (response.isSuccess()) {
+                        LOG.trace("{} receives {} for {}", session, response, writeACLRequest);
+                        sessionManager.onResponseSuccess(session, writeACLRequest);
+                        afterWritedAcls(session, cfg, aclInstancesToWrite, BootstrapPolicy.CONTINUE);
+                    } else {
+                        LOG.debug("{} receives {} for {}", session, response, writeACLRequest);
+                        BootstrapPolicy policy = sessionManager.onResponseError(session, writeACLRequest, response);
+                        afterWritedAcls(session, cfg, aclInstancesToWrite, policy);
+                    }
                 }
-            }, new ErrorCallback() {
+            }, new SafeErrorCallback(session) {
                 @Override
-                public void onError(Exception e) {
-                    LOG.debug(String.format(
-                            "Error during bootstrap write of acl instance %s to foreign peer [ep=%s,id=%s]",
-                            aclInstance, session.getEndpoint(), session.getIdentity()), e);
-                    sessionManager.failed(session, WRITE_ACL_FAILED, writeACLRequest);
+                public void safeOnError(Exception e) {
+                    LOG.debug("Error for {} while sending {} ", session, writeACLRequest, e);
+                    BootstrapPolicy policy = sessionManager.onRequestFailure(session, writeACLRequest, e);
+                    afterWritedAcls(session, cfg, aclInstancesToWrite, policy);
                 }
             });
         } else {
@@ -255,32 +387,123 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         }
     }
 
+    protected void afterWritedAcls(BootstrapSession session, BootstrapConfig cfg, List<Integer> aclInstancesToWrite,
+            BootstrapPolicy policy) {
+        switch (policy) {
+        case CONTINUE:
+            aclInstancesToWrite.remove(0);
+            writedAcls(session, cfg, aclInstancesToWrite);
+            break;
+        case RETRY:
+            writedAcls(session, cfg, aclInstancesToWrite);
+            break;
+        case RETRYALL:
+            startBootstrap(session, cfg);
+            break;
+        case SEND_FINISHED:
+            bootstrapFinished(session, cfg);
+            break;
+        case STOP:
+            stopSession(session, WRITE_ACL_FAILED);
+            break;
+        default:
+            throw new IllegalStateException("unknown policy :" + policy);
+        }
+    }
+
     protected void bootstrapFinished(final BootstrapSession session, final BootstrapConfig cfg) {
 
         final BootstrapFinishRequest finishBootstrapRequest = new BootstrapFinishRequest();
-        send(session, finishBootstrapRequest, new ResponseCallback<BootstrapFinishResponse>() {
+        send(session, finishBootstrapRequest, new SafeResponseCallback<BootstrapFinishResponse>(session) {
             @Override
-            public void onResponse(BootstrapFinishResponse response) {
-                LOG.trace("Bootstrap Finished {} return code {}", session.getEndpoint(), response.getCode());
+            public void safeOnResponse(BootstrapFinishResponse response) {
                 if (response.isSuccess()) {
-                    sessionManager.end(session);
+                    LOG.trace("{} receives {} for {}", session, response, finishBootstrapRequest);
+                    sessionManager.onResponseSuccess(session, finishBootstrapRequest);
+                    afterBootstrapFinished(session, cfg, BootstrapPolicy.CONTINUE);
                 } else {
-                    sessionManager.failed(session, FINISHED_WITH_ERROR, finishBootstrapRequest);
+                    LOG.debug("{} receives {} for {}", session, response, finishBootstrapRequest);
+                    BootstrapPolicy policy = sessionManager.onResponseError(session, finishBootstrapRequest, response);
+                    afterBootstrapFinished(session, cfg, policy);
                 }
             }
-        }, new ErrorCallback() {
+        }, new SafeErrorCallback(session) {
             @Override
-            public void onError(Exception e) {
-                LOG.debug(String.format("Error during bootstrap finished to foreign peer [ep=%s,id=%s]",
-                        session.getEndpoint(), session.getIdentity()), e);
-                sessionManager.failed(session, SEND_FINISH_FAILED, finishBootstrapRequest);
+            public void safeOnError(Exception e) {
+                LOG.debug("Error for {} while sending {} ", session, finishBootstrapRequest, e);
+                BootstrapPolicy policy = sessionManager.onRequestFailure(session, finishBootstrapRequest, e);
+                afterBootstrapFinished(session, cfg, policy);
             }
         });
     }
 
+    protected void afterBootstrapFinished(BootstrapSession session, BootstrapConfig cfg, BootstrapPolicy policy) {
+        switch (policy) {
+        case CONTINUE:
+            stopSession(session, null);
+            break;
+        case RETRY:
+            bootstrapFinished(session, cfg);
+            break;
+        case RETRYALL:
+            startBootstrap(session, cfg);
+            break;
+        case SEND_FINISHED:
+            bootstrapFinished(session, cfg);
+            break;
+        case STOP:
+            stopSession(session, FINISH_FAILED);
+            break;
+        default:
+            throw new IllegalStateException("unknown policy :" + policy);
+        }
+    }
+
     protected <T extends LwM2mResponse> void send(BootstrapSession session, DownlinkRequest<T> request,
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
         sender.send(session.getEndpoint(), session.getIdentity(), request, requestTimeout, responseCallback,
                 errorCallback);
     }
+
+    protected abstract class SafeResponseCallback<T extends LwM2mResponse> implements ResponseCallback<T> {
+
+        private BootstrapSession session;
+
+        public SafeResponseCallback(BootstrapSession session) {
+            this.session = session;
+        }
+
+        @Override
+        public void onResponse(T response) {
+            try {
+                safeOnResponse(response);
+            } catch (RuntimeException e) {
+                LOG.warn("Unexpected error on response callback for {}", session, e);
+                stopSession(session, INTERNAL_SERVER_ERROR);
+            }
+        }
+
+        public abstract void safeOnResponse(T response);
+    }
+
+    protected abstract class SafeErrorCallback implements ErrorCallback {
+
+        private BootstrapSession session;
+
+        public SafeErrorCallback(BootstrapSession session) {
+            this.session = session;
+        }
+
+        @Override
+        public void onError(Exception error) {
+            try {
+                safeOnError(error);
+            } catch (RuntimeException e) {
+                LOG.warn("Unexpected error on error callback for {}", session, e);
+                stopSession(session, INTERNAL_SERVER_ERROR);
+            }
+        }
+
+        public abstract void safeOnError(Exception e);
+    }
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSession.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSession.java
index c167ac96..a6ecd0c4 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSession.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSession.java
@@ -19,23 +19,58 @@ import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.server.bootstrap.BootstrapSession;
 
+/**
+ * A default implementation for {@link BootstrapSession}
+ */
 public class DefaultBootstrapSession implements BootstrapSession {
 
     private final String endpoint;
     private final Identity identity;
     private final boolean authorized;
     private final ContentFormat contentFormat;
+    private final long creationTime;
 
+    /**
+     * Create a {@link DefaultBootstrapSession} using default {@link ContentFormat#TLV} content format and using
+     * <code>System.currentTimeMillis()</code> to set the creation time.
+     * 
+     * @param endpoint The endpoint of the device.
+     * @param identity The transport layer identity of the device.
+     * @param authorized True if device is authorized to bootstrap.
+     */
     public DefaultBootstrapSession(String endpoint, Identity identity, boolean authorized) {
         this(endpoint, identity, authorized, ContentFormat.TLV);
     }
 
+    /**
+     * Create a {@link DefaultBootstrapSession} using <code>System.currentTimeMillis()</code> to set the creation time.
+     * 
+     * @param endpoint The endpoint of the device.
+     * @param identity The transport layer identity of the device.
+     * @param authorized True if device is authorized to bootstrap.
+     * @param contentFormat The content format to use to write object.
+     */
     public DefaultBootstrapSession(String endpoint, Identity identity, boolean authorized,
             ContentFormat contentFormat) {
+        this(endpoint, identity, authorized, contentFormat, System.currentTimeMillis());
+    }
+
+    /**
+     * Create a {@link DefaultBootstrapSession}.
+     * 
+     * @param endpoint The endpoint of the device.
+     * @param identity The transport layer identity of the device.
+     * @param authorized True if device is authorized to bootstrap.
+     * @param contentFormat The content format to use to write object.
+     * @param creationTime The creation time of this session in ms.
+     */
+    public DefaultBootstrapSession(String endpoint, Identity identity, boolean authorized, ContentFormat contentFormat,
+            long creationTime) {
         this.endpoint = endpoint;
         this.identity = identity;
         this.authorized = authorized;
         this.contentFormat = contentFormat;
+        this.creationTime = creationTime;
     }
 
     @Override
@@ -58,10 +93,16 @@ public class DefaultBootstrapSession implements BootstrapSession {
         return contentFormat;
     }
 
+    @Override
+    public long getCreationTime() {
+        return creationTime;
+    }
+
     @Override
     public String toString() {
-        return String.format("BootstrapSession [endpoint=%s, identity=%s, authorized=%s, contentFormat=%s]", endpoint,
-                identity, authorized, contentFormat);
+        return String.format(
+                "BootstrapSession [endpoint=%s, identity=%s, authorized=%s, contentFormat=%s, creationTime=%dms]",
+                endpoint, identity, authorized, contentFormat, creationTime);
     }
 
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSessionManager.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSessionManager.java
index 8b51f888..686a4675 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSessionManager.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/DefaultBootstrapSessionManager.java
@@ -17,8 +17,9 @@ package org.eclipse.leshan.server.impl;
 
 import java.util.List;
 
-import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapFinishRequest;
 import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.request.LwM2mRequest;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause;
 import org.eclipse.leshan.server.bootstrap.BootstrapSession;
@@ -78,7 +79,25 @@ public class DefaultBootstrapSessionManager implements BootstrapSessionManager {
     }
 
     @Override
-    public void failed(BootstrapSession bsSession, BootstrapFailureCause cause,
-            DownlinkRequest<? extends LwM2mResponse> request) {
+    public void failed(BootstrapSession bsSession, BootstrapFailureCause cause) {
+    }
+
+    @Override
+    public void onResponseSuccess(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request) {
+    }
+
+    @Override
+    public BootstrapPolicy onResponseError(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+        if (request instanceof BootstrapFinishRequest) {
+            return BootstrapPolicy.STOP;
+        }
+        return BootstrapPolicy.CONTINUE;
+    }
+
+    @Override
+    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+            Throwable cause) {
+        return BootstrapPolicy.STOP;
     }
 }


Test patch:
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
index 9a418325..8b8c301c 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
@@ -18,7 +18,6 @@ package org.eclipse.leshan.server.bootstrap;
 import static org.junit.Assert.*;
 
 import java.net.InetSocketAddress;
-import java.util.concurrent.Executor;
 
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
@@ -27,6 +26,7 @@ import org.eclipse.leshan.core.request.BootstrapRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.request.LwM2mRequest;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapFinishResponse;
 import org.eclipse.leshan.core.response.BootstrapResponse;
@@ -34,6 +34,8 @@ import org.eclipse.leshan.core.response.BootstrapWriteResponse;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.core.response.SendableResponse;
+import org.eclipse.leshan.server.bootstrap.BootstrapHandlerTest.MockRequestSender.Mode;
 import org.eclipse.leshan.server.impl.DefaultBootstrapSession;
 import org.junit.Test;
 
@@ -41,70 +43,149 @@ public class BootstrapHandlerTest {
 
     @Test
     public void error_if_not_authorized() {
-        final BootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(false);
-
+        // prepare bootstrapHandler with a session manager which does not authorized any session
+        BootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(false);
         BootstrapHandler bsHandler = new DefaultBootstrapHandler(null, null, bsSessionManager);
-        BootstrapResponse bootstrapResponse = bsHandler
-                .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("enpoint"));
-        assertEquals(ResponseCode.BAD_REQUEST, bootstrapResponse.getCode());
+
+        // Try to bootstrap
+        BootstrapResponse response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("endpoint"))
+                .getResponse();
+
+        // Ensure bootstrap session is refused
+        assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
     }
 
     @Test
-    public void notifies_at_end_of_successful_bootstrap() {
-        final MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true);
-        final LwM2mBootstrapRequestSender requestSender = new MockRequestSender(true);
-
-        final BootstrapConfigStore bsStore = new BootstrapConfigStore() {
-            @Override
-            public BootstrapConfig get(String endpoint, Identity deviceIdentity) {
-                return new BootstrapConfig();
-            }
-        };
+    public void bootstrap_success() throws InvalidConfigurationException {
+        // prepare a bootstrap handler with a session manager which authorize all session
+        // and a sender which "obtains" always successful response.
+        // and a config store with and an empty config for the expected endpoint
+        MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true);
+        LwM2mBootstrapRequestSender requestSender = new MockRequestSender(Mode.ALWAYS_SUCCESS);
+        EditableBootstrapConfigStore bsStore = new InMemoryBootstrapConfigStore();
+        bsStore.add("endpoint", new BootstrapConfig());
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager);
 
-        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
+        // Try to bootstrap
+        SendableResponse<BootstrapResponse> sendableResponse = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("endpoint"));
+        sendableResponse.sent();
 
-            @Override
-            public void execute(Runnable command) {
-                command.run();
-            }
-        });
+        // Ensure bootstrap finished
+        assertTrue(bsSessionManager.endWasCalled());
+        assertFalse(bsSessionManager.failedWasCalled());
+    }
+
+    @Test
+    public void bootstrap_failed_because_of_sent_failure() throws InvalidConfigurationException {
+        // prepare a bootstrap handler with a session manager which authorize all session
+        // and a sender which always failed to send request.
+        // and a config store with and an empty config for the expected endpoint
+        MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true);
+        LwM2mBootstrapRequestSender requestSender = new MockRequestSender(Mode.ALWAYS_FAILURE);
+        EditableBootstrapConfigStore bsStore = new InMemoryBootstrapConfigStore();
+        bsStore.add("endpoint", new BootstrapConfig());
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager);
 
-        bsHandler.bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("enpoint"));
+        // Try to bootstrap
+        SendableResponse<BootstrapResponse> sendableResponse = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("endpoint"));
+        sendableResponse.sent();
 
-        assertTrue(bsSessionManager.endWasCalled());
+        // Ensure bootstrap failed
+        assertFalse(bsSessionManager.endWasCalled());
+        assertTrue(bsSessionManager.failedWasCalled());
+        assertEquals(BootstrapFailureCause.FINISH_FAILED, bsSessionManager.failureCause);
     }
 
     @Test
-    public void does_not_notifies_at_end_of_failed_bootstrap() {
-        final MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true);
-        final LwM2mBootstrapRequestSender requestSender = new MockRequestSender(false);
-
-        final BootstrapConfigStore bsStore = new BootstrapConfigStore() {
-            @Override
-            public BootstrapConfig get(String endpoint, Identity deviceIdentity) {
-                return new BootstrapConfig();
-            }
-        };
+    public void two_bootstrap_at_the_same_time_not_allowed()
+            throws InvalidConfigurationException, InterruptedException {
+        // prepare a bootstrap handler with a session manager which authorize all session
+        // and a sender which never get response.
+        // and a config store with and an empty config for the expected endpoint
+        // and lifetime session of 1s
+        MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true);
+        MockRequestSender requestSender = new MockRequestSender(Mode.NO_RESPONSE);
+        EditableBootstrapConfigStore bsStore = new InMemoryBootstrapConfigStore();
+        bsStore.add("endpoint", new BootstrapConfig());
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager,
+                DefaultBootstrapHandler.DEFAULT_TIMEOUT, 1000);
 
-        BootstrapHandler bsHandler = new DefaultBootstrapHandler(bsStore, requestSender, bsSessionManager, new Executor() {
+        // First bootstrap : which will not end (because of sender)
+        SendableResponse<BootstrapResponse> first_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("endpoint"));
+        first_response.sent();
+        // Ensure bootstrap is accepted and not finished
+        assertTrue(first_response.getResponse().isSuccess());
+        assertFalse(bsSessionManager.endWasCalled());
+        assertFalse(bsSessionManager.failedWasCalled());
 
-            @Override
-            public void execute(Runnable command) {
-                command.run();
-            }
-        });
+        // Second bootstrap for same endpoint : must be refused
+        bsSessionManager.reset();
+        SendableResponse<BootstrapResponse> second_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4243), "pskdentity"), new BootstrapRequest("endpoint"));
+        second_response.sent();
+        assertTrue(second_response.getResponse().isFailure());
+        assertFalse(bsSessionManager.endWasCalled());
+        assertTrue(bsSessionManager.failedWasCalled());
 
-        bsHandler.bootstrap(Identity.psk(new InetSocketAddress(4242), "pskdentity"), new BootstrapRequest("enpoint"));
+        // Third bootstrap for same endpoint : must be refused
+        bsSessionManager.reset();
+        SendableResponse<BootstrapResponse> third_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4243), "pskdentity"), new BootstrapRequest("endpoint"));
+        third_response.sent();
+        assertTrue(third_response.getResponse().isFailure());
+        assertFalse(bsSessionManager.endWasCalled());
+        assertTrue(bsSessionManager.failedWasCalled());
 
+        // Wait session lifetime
+        Thread.sleep(1000);
+
+        // Fourth bootstrap for same endpoint : now it should be accepted
+        bsSessionManager.reset();
+        requestSender.setMode(Mode.ALWAYS_SUCCESS);
+        SendableResponse<BootstrapResponse> fourth_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4243), "pskdentity"), new BootstrapRequest("endpoint"));
+        fourth_response.sent();
+        assertTrue(fourth_response.getResponse().isSuccess());
+        assertTrue(bsSessionManager.endWasCalled());
+        assertFalse(bsSessionManager.failedWasCalled());
+
+        // ensure that after a finished success bootstrap I don't need to wait session lifetime to retry
+        // fifth bootstrap for same endpoint : it should be accepted too.
+        bsSessionManager.reset();
+        requestSender.setMode(Mode.ALWAYS_FAILURE);
+        SendableResponse<BootstrapResponse> fifth_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4243), "pskdentity"), new BootstrapRequest("endpoint"));
+        fifth_response.sent();
+        assertTrue(fifth_response.getResponse().isSuccess());
         assertFalse(bsSessionManager.endWasCalled());
+        assertTrue(bsSessionManager.failedWasCalled());
+
+        // ensure that after a failed bootstrap I don't need to wait session lifetime to retry
+        // sixth bootstrap for same endpoint : it should be accepted too.
+        bsSessionManager.reset();
+        requestSender.setMode(Mode.ALWAYS_SUCCESS);
+        SendableResponse<BootstrapResponse> sixth_response = bsHandler
+                .bootstrap(Identity.psk(new InetSocketAddress(4243), "pskdentity"), new BootstrapRequest("endpoint"));
+        sixth_response.sent();
+        assertTrue(sixth_response.getResponse().isSuccess());
+        assertTrue(bsSessionManager.endWasCalled());
+        assertFalse(bsSessionManager.failedWasCalled());
     }
 
-    private class MockRequestSender implements LwM2mBootstrapRequestSender {
+    static class MockRequestSender implements LwM2mBootstrapRequestSender {
 
-        private boolean success;
+        public enum Mode {
+            ALWAYS_SUCCESS, ALWAYS_FAILURE, NO_RESPONSE
+        };
+
+        private Mode mode;
 
-        public MockRequestSender(boolean success) {
-            this.success = success;
+        public MockRequestSender(Mode mode) {
+            this.mode = mode;
         }
 
         @Override
@@ -113,33 +194,51 @@ public class BootstrapHandlerTest {
             return null;
         }
 
+        public void setMode(Mode mode) {
+            this.mode = mode;
+        }
+
         @SuppressWarnings("unchecked")
         @Override
         public <T extends LwM2mResponse> void send(String clientEndpoint, Identity destination,
                 DownlinkRequest<T> request, long timeout, ResponseCallback<T> responseCallback,
                 ErrorCallback errorCallback) {
-            if (request instanceof BootstrapDeleteRequest) {
-                ((ResponseCallback<BootstrapDeleteResponse>) responseCallback)
-                        .onResponse(BootstrapDeleteResponse.success());
-            } else if (request instanceof BootstrapWriteRequest) {
-                ((ResponseCallback<BootstrapWriteResponse>) responseCallback)
-                        .onResponse(BootstrapWriteResponse.success());
-            } else if (request instanceof BootstrapFinishRequest) {
-                if (this.success) {
+            // no response, no callback call
+            if (mode == Mode.NO_RESPONSE) {
+                return;
+            }
+
+            if (mode == Mode.ALWAYS_SUCCESS) {
+                if (request instanceof BootstrapDeleteRequest) {
+                    ((ResponseCallback<BootstrapDeleteResponse>) responseCallback)
+                            .onResponse(BootstrapDeleteResponse.success());
+                } else if (request instanceof BootstrapWriteRequest) {
+                    ((ResponseCallback<BootstrapWriteResponse>) responseCallback)
+                            .onResponse(BootstrapWriteResponse.success());
+                } else if (request instanceof BootstrapFinishRequest) {
                     ((ResponseCallback<BootstrapFinishResponse>) responseCallback)
                             .onResponse(BootstrapFinishResponse.success());
-                } else {
+                }
+            } else if (mode == Mode.ALWAYS_FAILURE) {
+                if (request instanceof BootstrapDeleteRequest) {
+                    ((ResponseCallback<BootstrapDeleteResponse>) responseCallback)
+                            .onResponse(BootstrapDeleteResponse.internalServerError("delete failed"));
+                } else if (request instanceof BootstrapWriteRequest) {
+                    ((ResponseCallback<BootstrapWriteResponse>) responseCallback)
+                            .onResponse(BootstrapWriteResponse.internalServerError("write failed"));
+                } else if (request instanceof BootstrapFinishRequest) {
                     ((ResponseCallback<BootstrapFinishResponse>) responseCallback)
-                            .onResponse(BootstrapFinishResponse.internalServerError("failed"));
+                            .onResponse(BootstrapFinishResponse.internalServerError("finished failed"));
                 }
             }
         }
     }
 
-    private class MockBootstrapSessionManager implements BootstrapSessionManager {
+    private static class MockBootstrapSessionManager implements BootstrapSessionManager {
 
         private boolean authorized;
         private BootstrapSession endBsSession = null;
+        private BootstrapFailureCause failureCause = null;
 
         public MockBootstrapSessionManager(boolean authorized) {
             this.authorized = authorized;
@@ -159,9 +258,38 @@ public class BootstrapHandlerTest {
             return this.endBsSession != null;
         }
 
+        public boolean failedWasCalled() {
+            return this.failureCause != null;
+        }
+
+        @Override
+        public void failed(BootstrapSession bsSession, BootstrapFailureCause cause) {
+            failureCause = cause;
+        }
+
+        @Override
+        public void onResponseSuccess(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request) {
+
+        }
+
         @Override
-        public void failed(BootstrapSession bsSession, BootstrapFailureCause cause,
-                DownlinkRequest<? extends LwM2mResponse> request) {
+        public BootstrapPolicy onResponseError(BootstrapSession bsSession,
+                LwM2mRequest<? extends LwM2mResponse> request, LwM2mResponse response) {
+            if (request instanceof BootstrapFinishRequest) {
+                return BootstrapPolicy.STOP;
+            }
+            return BootstrapPolicy.CONTINUE;
+        }
+
+        @Override
+        public BootstrapPolicy onRequestFailure(BootstrapSession bsSession,
+                LwM2mRequest<? extends LwM2mResponse> request, Throwable cause) {
+            return BootstrapPolicy.STOP;
+        }
+
+        public void reset() {
+            endBsSession = null;
+            failureCause = null;
         }
     }
 }