Repository: eclipse/leshan
Pull request #750 (2019-10-03 13:37:23)

Priority to last session on concurent bootstrap
Recently we add a mechanism to avoid to have 2 simultaneous bootstrap session by giving priority to the first session.

We finally think that this make more sense to give priority to the last one. This PR aims to do that.


Patch:
diff --git a/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/AsyncRequestObserver.java b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/AsyncRequestObserver.java
index 37282da1..d1499a17 100644
--- a/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/AsyncRequestObserver.java
+++ b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/AsyncRequestObserver.java
@@ -24,13 +24,18 @@ import org.eclipse.leshan.core.response.ResponseCallback;
 public abstract class AsyncRequestObserver<T extends LwM2mResponse> extends CoapAsyncRequestObserver {
 
     public AsyncRequestObserver(Request coapRequest, final ResponseCallback<T> responseCallback,
-            ErrorCallback errorCallback, long timeoutInMs) {
+            final ErrorCallback errorCallback, long timeoutInMs) {
         super(coapRequest, null, errorCallback, timeoutInMs);
         this.responseCallback = new CoapResponseCallback() {
 
             @Override
             public void onResponse(Response coapResponse) {
-                T lwM2mResponseT = buildResponse(coapResponse);
+                T lwM2mResponseT = null;
+                try {
+                    lwM2mResponseT = buildResponse(coapResponse);
+                } catch (Exception e) {
+                    errorCallback.onError(e);
+                }
                 if (lwM2mResponseT != null) {
                     responseCallback.onResponse(lwM2mResponseT);
                 }
diff --git a/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/CoapAsyncRequestObserver.java b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/CoapAsyncRequestObserver.java
index 77ffb5bd..185797e8 100644
--- a/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/CoapAsyncRequestObserver.java
+++ b/leshan-core-cf/src/main/java/org/eclipse/leshan/core/californium/CoapAsyncRequestObserver.java
@@ -45,6 +45,13 @@ public class CoapAsyncRequestObserver extends AbstractRequestObserver {
     private ScheduledFuture<?> cleaningTask;
     private boolean cancelled = false;
 
+    // The Californium API does not ensure that message callback are exclusive
+    // meaning that you can get a onReponse call and a onCancel one.
+    // The CoapAsyncRequestObserver ensure that you will receive only one event.
+    // You get either 1 response or 1 error.
+    // This boolean is used to ensure this.
+    private AtomicBoolean eventRaised = new AtomicBoolean(false);
+
     private final AtomicBoolean responseTimedOut = new AtomicBoolean(false);
 
     public CoapAsyncRequestObserver(Request coapRequest, CoapResponseCallback responseCallback,
@@ -58,13 +65,18 @@ public class CoapAsyncRequestObserver extends AbstractRequestObserver {
     @Override
     public void onResponse(Response coapResponse) {
         LOG.debug("Received coap response: {} for {}", coapResponse, coapRequest);
-        try {
+        coapRequest.removeMessageObserver(this);
+        if (eventRaised.compareAndSet(false, true)) {
             cleaningTask.cancel(false);
-            responseCallback.onResponse(coapResponse);
-        } catch (Exception e) {
-            errorCallback.onError(e);
-        } finally {
-            coapRequest.removeMessageObserver(this);
+            try {
+                responseCallback.onResponse(coapResponse);
+            } catch (RuntimeException e) {
+                LOG.warn("Uncaught exception during onResponse callback");
+            }
+
+        } else {
+            LOG.debug("OnResponse callback ignored because an event was already raised for this request {}",
+                    coapRequest);
         }
     }
 
@@ -76,30 +88,51 @@ public class CoapAsyncRequestObserver extends AbstractRequestObserver {
     @Override
     public void onTimeout() {
         cancelCleaningTask();
-        errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException("Request %s timed out",
-                coapRequest.getURI()));
+        if (eventRaised.compareAndSet(false, true)) {
+            errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException("Request %s timed out",
+                    coapRequest.getURI()));
+        } else {
+            LOG.debug("OnTimeout callback ignored because an event was already raised for this request {}",
+                    coapRequest);
+        }
     }
 
     @Override
     public void onCancel() {
         cancelCleaningTask();
-        if (responseTimedOut.get())
-            errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException("Request %s timed out",
-                    coapRequest.getURI()));
-        else
-            errorCallback.onError(new RequestCanceledException("Request %s cancelled", coapRequest.getURI()));
+        if (eventRaised.compareAndSet(false, true)) {
+            if (responseTimedOut.get()) {
+                errorCallback.onError(new org.eclipse.leshan.core.request.exception.TimeoutException(
+                        "Request %s timed out", coapRequest.getURI()));
+            } else {
+                errorCallback.onError(new RequestCanceledException("Request %s cancelled", coapRequest.getURI()));
+            }
+        } else {
+            LOG.debug(
+                    "OnCancel(responsetimeout={}) callback ignored because an event was already raised for this request {}",
+                    responseTimedOut.get(), coapRequest);
+        }
     }
 
     @Override
     public void onReject() {
         cancelCleaningTask();
-        errorCallback.onError(new RequestRejectedException("Request %s rejected", coapRequest.getURI()));
+        if (eventRaised.compareAndSet(false, true)) {
+            errorCallback.onError(new RequestRejectedException("Request %s rejected", coapRequest.getURI()));
+        } else {
+            LOG.debug("OnReject callback ignored because an event was already raised for this request {}", coapRequest);
+        }
     }
 
     @Override
     public void onSendError(Throwable error) {
         cancelCleaningTask();
-        errorCallback.onError(new SendFailedException(error, "Unable to send request %s", coapRequest.getURI()));
+        if (eventRaised.compareAndSet(false, true)) {
+            errorCallback.onError(new SendFailedException(error, "Unable to send request %s", coapRequest.getURI()));
+        } else {
+            LOG.debug("onSendError callback ignored because an event was already raised for this request {}",
+                    coapRequest);
+        }
     }
 
     private synchronized void scheduleCleaningTask() {
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
index 15917d38..416c4d35 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServer.java
@@ -18,8 +18,6 @@ package org.eclipse.leshan.server.californium;
 
 import java.net.InetSocketAddress;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 
 import org.eclipse.californium.core.CoapResource;
 import org.eclipse.californium.core.CoapServer;
@@ -142,21 +140,18 @@ public class LeshanServer {
         Validate.notNull(registrationIdProvider, "registrationIdProvider cannot be null");
 
         // Create CoAP server
-        Set<Endpoint> endpoints = new HashSet<>();
         coapServer = createCoapServer(coapConfig);
 
         // unsecured endpoint
         this.unsecuredEndpoint = unsecuredEndpoint;
         if (unsecuredEndpoint != null) {
             coapServer.addEndpoint(unsecuredEndpoint);
-            endpoints.add(unsecuredEndpoint);
         }
 
         // secure endpoint
         this.securedEndpoint = securedEndpoint;
         if (securedEndpoint != null) {
             coapServer.addEndpoint(securedEndpoint);
-            endpoints.add(securedEndpoint);
         }
 
         // init services and stores
@@ -176,8 +171,8 @@ public class LeshanServer {
         coapServer.add(createRegisterResource(registrationService, authorizer, registrationIdProvider));
 
         // create request sender
-        requestSender = createRequestSender(endpoints, registrationService, observationService, this.modelProvider,
-                encoder, decoder, presenceService);
+        requestSender = createRequestSender(securedEndpoint, unsecuredEndpoint, registrationService, observationService,
+                this.modelProvider, encoder, decoder, presenceService);
 
         coapApi = new CoapAPI();
     }
@@ -226,7 +221,7 @@ public class LeshanServer {
         return new RegisterResource(new RegistrationHandler(registrationService, authorizer, registrationIdProvider));
     }
 
-    protected LwM2mRequestSender createRequestSender(Set<Endpoint> endpoints,
+    protected LwM2mRequestSender createRequestSender(Endpoint securedEndpoint, Endpoint unsecuredEndpoint,
             RegistrationServiceImpl registrationService, ObservationServiceImpl observationService,
             LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
             PresenceServiceImpl presenceService) {
@@ -234,11 +229,11 @@ public class LeshanServer {
         // if no queue mode, create a "simple" sender
         final LwM2mRequestSender requestSender;
         if (presenceService == null)
-            requestSender = new CaliforniumLwM2mRequestSender(endpoints, observationService, modelProvider, encoder,
-                    decoder);
+            requestSender = new CaliforniumLwM2mRequestSender(securedEndpoint, unsecuredEndpoint, observationService,
+                    modelProvider, encoder, decoder);
         else
-            requestSender = new CaliforniumQueueModeRequestSender(presenceService,
-                    new CaliforniumLwM2mRequestSender(endpoints, observationService, modelProvider, encoder, decoder));
+            requestSender = new CaliforniumQueueModeRequestSender(presenceService, new CaliforniumLwM2mRequestSender(
+                    securedEndpoint, unsecuredEndpoint, observationService, modelProvider, encoder, decoder));
 
         // Cancel observations on client unregistering
         registrationService.addListener(new RegistrationListener() {
@@ -247,14 +242,14 @@ public class LeshanServer {
             public void updated(RegistrationUpdate update, Registration updatedRegistration, Registration previousReg) {
                 if (!update.getAddress().equals(previousReg.getAddress())
                         || update.getPort() != previousReg.getPort()) {
-                    requestSender.cancelPendingRequests(previousReg);
+                    requestSender.cancelOngoingRequests(previousReg);
                 }
             }
 
             @Override
             public void unregistered(Registration registration, Collection<Observation> observations, boolean expired,
                     Registration newReg) {
-                requestSender.cancelPendingRequests(registration);
+                requestSender.cancelOngoingRequests(registration);
             }
 
             @Override
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
index 3af49a92..14110225 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/CaliforniumLwM2mBootstrapRequestSender.java
@@ -15,112 +15,50 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.californium.bootstrap;
 
-import org.eclipse.californium.core.coap.MessageObserver;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
 import org.eclipse.californium.core.network.Endpoint;
-import org.eclipse.leshan.core.californium.AsyncRequestObserver;
-import org.eclipse.leshan.core.californium.SyncRequestObserver;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.DownlinkRequest;
-import org.eclipse.leshan.core.request.Identity;
 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.server.bootstrap.BootstrapSession;
 import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender;
-import org.eclipse.leshan.server.californium.request.CoapRequestBuilder;
-import org.eclipse.leshan.server.californium.request.LwM2mResponseBuilder;
-import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.server.californium.request.RequestSender;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapRequestSender {
+
     static final Logger LOG = LoggerFactory.getLogger(CaliforniumLwM2mBootstrapRequestSender.class);
 
-    private final Endpoint nonSecureEndpoint;
-    private final Endpoint secureEndpoint;
     private final LwM2mModel model;
-    private final LwM2mNodeDecoder decoder;
-    private final LwM2mNodeEncoder encoder;
+
+    private final RequestSender sender;
 
     public CaliforniumLwM2mBootstrapRequestSender(Endpoint secureEndpoint, Endpoint nonSecureEndpoint, LwM2mModel model,
             LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder) {
-        this.secureEndpoint = secureEndpoint;
-        this.nonSecureEndpoint = nonSecureEndpoint;
         this.model = model;
-        this.encoder = encoder;
-        this.decoder = decoder;
+        this.sender = new RequestSender(secureEndpoint, nonSecureEndpoint, encoder, decoder);
     }
 
     @Override
-    public <T extends LwM2mResponse> T send(final String endpointName, final Identity destination,
-            final DownlinkRequest<T> request, long timeout) throws InterruptedException { // Create the CoAP request
-                                                                                          // from LwM2m request
-        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, model, encoder);
-        request.accept(coapClientRequestBuilder);
-
-        final Request coapRequest = coapClientRequestBuilder.getRequest();
-
-        // Send CoAP request synchronously
-        SyncRequestObserver<T> syncMessageObserver = new SyncRequestObserver<T>(coapRequest, timeout) {
-            @Override
-            public T buildResponse(Response coapResponse) {
-                // TODO we need to fix that by removing the Client dependency from LwM2MResponseBuilder or by creating a
-                // LwM2mBootstrapResponseBuilder
-                Registration registration = new Registration.Builder("fakeregistrationid", endpointName, destination,
-                        destination.isSecure() ? secureEndpoint.getAddress() : nonSecureEndpoint.getAddress()).build();
-                // Build LwM2m response
-                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
-                        registration, model, null, decoder);
-                request.accept(lwm2mResponseBuilder);
-                return lwm2mResponseBuilder.getResponse();
-            }
-        };
-        coapRequest.addMessageObserver(syncMessageObserver);
-
-        // Send CoAP request asynchronously
-        if (destination.isSecure())
-            secureEndpoint.sendRequest(coapRequest);
-        else
-            nonSecureEndpoint.sendRequest(coapRequest);
-
-        // Wait for response, then return it
-        return syncMessageObserver.waitForResponse();
+    public <T extends LwM2mResponse> T send(BootstrapSession destination, DownlinkRequest<T> request, long timeout)
+            throws InterruptedException {
+        return sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model,
+                null, request, timeout);
     }
 
     @Override
-    public <T extends LwM2mResponse> void send(final String endpointName, final Identity destination,
-            final DownlinkRequest<T> request, final long timeout, ResponseCallback<T> responseCallback,
-            ErrorCallback errorCallback) {
-        // Create the CoAP request from LwM2m request
-        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, model, encoder);
-        request.accept(coapClientRequestBuilder);
-        final Request coapRequest = coapClientRequestBuilder.getRequest();
-
-        // Add CoAP request callback
-        MessageObserver obs = new AsyncRequestObserver<T>(coapRequest, responseCallback, errorCallback, timeout) {
-            @Override
-            public T buildResponse(Response coapResponse) {
-                // TODO we need to fix that by removing the Client dependency from LwM2MResponseBuilder or by creating a
-                // LwM2mBootstrapResponseBuilder
-                Registration registration = new Registration.Builder("fakeregistrationid", endpointName, destination,
-                        destination.isSecure() ? secureEndpoint.getAddress() : nonSecureEndpoint.getAddress()).build();
-
-                // Build LwM2m response
-                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
-                        registration, model, null, decoder);
-                request.accept(lwm2mResponseBuilder);
-                return lwm2mResponseBuilder.getResponse();
-            }
-        };
-        coapRequest.addMessageObserver(obs);
+    public <T extends LwM2mResponse> void send(BootstrapSession destination, DownlinkRequest<T> request, long timeout,
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+        sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model, null,
+                request, timeout, responseCallback, errorCallback);
+    }
 
-        // Send CoAP request asynchronously
-        if (destination.isSecure())
-            secureEndpoint.sendRequest(coapRequest);
-        else
-            nonSecureEndpoint.sendRequest(coapRequest);
+    @Override
+    public void cancelOngoingRequests(BootstrapSession destination) {
+        sender.cancelRequests(destination.getId());
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/RegisterResource.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/RegisterResource.java
index 7a0058cd..5cbe794d 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/RegisterResource.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/registration/RegisterResource.java
@@ -17,7 +17,6 @@ package org.eclipse.leshan.server.californium.registration;
 
 import static org.eclipse.leshan.core.californium.ResponseCodeUtil.toCoapResponseCode;
 
-import java.net.InetSocketAddress;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -162,9 +161,8 @@ public class RegisterResource extends LwM2mCoapResource {
 
         // Handle request
         // -------------------------------
-        InetSocketAddress serverEndpoint = exchange.advanced().getEndpoint().getAddress();
         final SendableResponse<RegisterResponse> sendableResponse = registrationHandler.register(sender,
-                registerRequest, serverEndpoint);
+                registerRequest);
         RegisterResponse response = sendableResponse.getResponse();
 
         // Create CoAP Response from LwM2m request
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
index 3a671f0c..f4b97034 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CaliforniumLwM2mRequestSender.java
@@ -16,31 +16,17 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.californium.request;
 
-import java.net.InetSocketAddress;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.concurrent.ConcurrentNavigableMap;
-import java.util.concurrent.ConcurrentSkipListMap;
-import java.util.concurrent.atomic.AtomicLong;
-
-import org.eclipse.californium.core.coap.MessageObserver;
-import org.eclipse.californium.core.coap.MessageObserverAdapter;
 import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.core.coap.Response;
 import org.eclipse.californium.core.network.Endpoint;
-import org.eclipse.californium.elements.EndpointContext;
-import org.eclipse.leshan.core.californium.AsyncRequestObserver;
-import org.eclipse.leshan.core.californium.CoapAsyncRequestObserver;
 import org.eclipse.leshan.core.californium.CoapResponseCallback;
-import org.eclipse.leshan.core.californium.CoapSyncRequestObserver;
-import org.eclipse.leshan.core.californium.EndpointContextUtil;
-import org.eclipse.leshan.core.californium.SyncRequestObserver;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.core.response.ObserveResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.server.californium.observation.ObservationServiceImpl;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
@@ -50,246 +36,78 @@ import org.eclipse.leshan.util.Validate;
 
 public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender, CoapRequestSender {
 
-    private final Set<Endpoint> endpoints;
     private final ObservationServiceImpl observationService;
     private final LwM2mModelProvider modelProvider;
-    private final LwM2mNodeDecoder decoder;
-    private final LwM2mNodeEncoder encoder;
-    // A map which contains all pending CoAP requests
-    // This is mainly used to cancel request and avoid retransmission on de-registration
-    private final ConcurrentNavigableMap<String/* registrationId#requestId */, Request /* pending coap Request */> pendingRequests = new ConcurrentSkipListMap<>();
+    private final RequestSender sender;
 
     /**
-     * @param endpoints the CoAP endpoints to use for sending requests
      * @param observationService the service for keeping track of observed resources
      * @param modelProvider provides the supported objects definitions
      */
-    public CaliforniumLwM2mRequestSender(Set<Endpoint> endpoints, ObservationServiceImpl observationService,
-            LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder) {
-        Validate.notNull(endpoints);
+    public CaliforniumLwM2mRequestSender(Endpoint secureEndpoint, Endpoint nonSecureEndpoint,
+            ObservationServiceImpl observationService, LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder,
+            LwM2mNodeDecoder decoder) {
         Validate.notNull(observationService);
         Validate.notNull(modelProvider);
         this.observationService = observationService;
-        this.endpoints = endpoints;
         this.modelProvider = modelProvider;
-        this.encoder = encoder;
-        this.decoder = decoder;
+        this.sender = new RequestSender(secureEndpoint, nonSecureEndpoint, encoder, decoder);
     }
 
     @Override
-    public <T extends LwM2mResponse> T send(final Registration destination, final DownlinkRequest<T> request,
-            long timeout) throws InterruptedException {
+    public <T extends LwM2mResponse> T send(Registration destination, DownlinkRequest<T> request, long timeout)
+            throws InterruptedException {
 
         // Retrieve the objects definition
         final LwM2mModel model = modelProvider.getObjectModel(destination);
 
-        // Create the CoAP request from LwM2m request
-        CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(destination.getIdentity(),
-                destination.getRootPath(), destination.getId(), destination.getEndpoint(), model, encoder);
-        request.accept(coapRequestBuilder);
-        final Request coapRequest = coapRequestBuilder.getRequest();
-
-        // Send CoAP request synchronously
-        SyncRequestObserver<T> syncMessageObserver = new SyncRequestObserver<T>(coapRequest, timeout) {
-            @Override
-            public T buildResponse(Response coapResponse) {
-                // Build LwM2m response
-                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
-                        destination, model, observationService, decoder);
-                request.accept(lwm2mResponseBuilder);
-                return lwm2mResponseBuilder.getResponse();
-            }
-        };
-        coapRequest.addMessageObserver(syncMessageObserver);
-
-        // Store pending request to cancel it on de-registration
-        addPendingRequest(destination.getId(), coapRequest);
+        // Send requests synchronously
+        T response = sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(),
+                model, destination.getRootPath(), request, timeout);
 
-        // Send CoAP request asynchronously
-        Endpoint endpoint = getEndpointForClient(destination);
-        endpoint.sendRequest(coapRequest);
-
-        // Wait for response, then return it
-        return syncMessageObserver.waitForResponse();
+        // Handle special observe case
+        if (response != null && response.getClass() == ObserveResponse.class && response.isSuccess()) {
+            observationService.addObservation(destination, ((ObserveResponse) response).getObservation());
+        }
+        return response;
     }
 
     @Override
-    public <T extends LwM2mResponse> void send(final Registration destination, final DownlinkRequest<T> request,
-            long timeout, ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+    public <T extends LwM2mResponse> void send(final Registration destination, DownlinkRequest<T> request, long timeout,
+            final ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
         // Retrieve the objects definition
         final LwM2mModel model = modelProvider.getObjectModel(destination);
 
-        // Create the CoAP request from LwM2m request
-        CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(destination.getIdentity(),
-                destination.getRootPath(), destination.getId(), destination.getEndpoint(), model, encoder);
-        request.accept(coapRequestBuilder);
-        final Request coapRequest = coapRequestBuilder.getRequest();
-
-        // Add CoAP request callback
-        MessageObserver obs = new AsyncRequestObserver<T>(coapRequest, responseCallback, errorCallback, timeout) {
-            @Override
-            public T buildResponse(Response coapResponse) {
-                // Build LwM2m response
-                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
-                        destination, model, observationService, decoder);
-                request.accept(lwm2mResponseBuilder);
-                return lwm2mResponseBuilder.getResponse();
-            }
-        };
-        coapRequest.addMessageObserver(obs);
-
-        // Store pending request to cancel it on de-registration
-        addPendingRequest(destination.getId(), coapRequest);
-
-        // Send CoAP request asynchronously
-        Endpoint endpoint = getEndpointForClient(destination);
-        endpoint.sendRequest(coapRequest);
+        // Send requests asynchronously
+        sender.sendLwm2mRequest(destination.getEndpoint(), destination.getIdentity(), destination.getId(), model,
+                destination.getRootPath(), request, timeout, new ResponseCallback<T>() {
+                    @Override
+                    public void onResponse(T response) {
+                        if (response != null && response.getClass() == ObserveResponse.class && response.isSuccess()) {
+                            observationService.addObservation(destination,
+                                    ((ObserveResponse) response).getObservation());
+                        }
+                        responseCallback.onResponse(response);
+                    }
+                }, errorCallback);
     }
 
     @Override
-    public Response sendCoapRequest(final Registration destination, final Request coapRequest, long timeout)
+    public Response sendCoapRequest(Registration destination, Request coapRequest, long timeout)
             throws InterruptedException {
-
-        // Define destination
-        EndpointContext context = EndpointContextUtil.extractContext(destination.getIdentity());
-        coapRequest.setDestinationContext(context);
-
-        // Send CoAP request synchronously
-        CoapSyncRequestObserver syncMessageObserver = new CoapSyncRequestObserver(coapRequest, timeout);
-        coapRequest.addMessageObserver(syncMessageObserver);
-
-        // Store pending request to cancel it on de-registration
-        addPendingRequest(destination.getId(), coapRequest);
-
-        // Send CoAP request asynchronously
-        Endpoint endpoint = getEndpointForClient(destination);
-        endpoint.sendRequest(coapRequest);
-
-        // Wait for response, then return it
-        return syncMessageObserver.waitForCoapResponse();
+        return sender.sendCoapRequest(destination.getIdentity(), destination.getId(), coapRequest, timeout);
     }
 
     @Override
-    public void sendCoapRequest(final Registration destination, final Request coapRequest, long timeout,
+    public void sendCoapRequest(Registration destination, Request coapRequest, long timeout,
             CoapResponseCallback responseCallback, ErrorCallback errorCallback) {
-
-        // Define destination
-        EndpointContext context = EndpointContextUtil.extractContext(destination.getIdentity());
-        coapRequest.setDestinationContext(context);
-
-        // Add CoAP request callback
-        MessageObserver obs = new CoapAsyncRequestObserver(coapRequest, responseCallback, errorCallback, timeout);
-        coapRequest.addMessageObserver(obs);
-
-        // Store pending request to cancel it on de-registration
-        addPendingRequest(destination.getId(), coapRequest);
-
-        // Send CoAP request asynchronously
-        Endpoint endpoint = getEndpointForClient(destination);
-        endpoint.sendRequest(coapRequest);
+        sender.sendCoapRequest(destination.getIdentity(), destination.getId(), coapRequest, timeout, responseCallback,
+                errorCallback);
     }
 
     @Override
-    public void cancelPendingRequests(Registration registration) {
+    public void cancelOngoingRequests(Registration registration) {
         Validate.notNull(registration);
-        String registrationId = registration.getId();
-        SortedMap<String, Request> requests = pendingRequests.subMap(getFloorKey(registrationId),
-                getCeilingKey(registrationId));
-        for (Request coapRequest : requests.values()) {
-            coapRequest.cancel();
-        }
-        requests.clear();
-    }
-
-    private static String getFloorKey(String registrationId) {
-        // The key format is regid#long, So we need a key which is always before this pattern (in natural order).
-        return registrationId + '#';
-    }
-
-    private static String getCeilingKey(String registrationId) {
-        // The key format is regid#long, So we need a key which is always after this pattern (in natural order).
-        return registrationId + "#A";
-    }
-
-    private static String getKey(String registrationId, long requestId) {
-        return registrationId + '#' + requestId;
-    }
-
-    private void addPendingRequest(String registrationId, Request coapRequest) {
-        Validate.notNull(registrationId);
-        // Theoretically we should add observer only for CONFIRMABLE request but with transparent block-wise mode, an
-        // UNCONFIRMABLE request could be change in several block-wised requests.
-        CleanerMessageObserver observer = new CleanerMessageObserver(registrationId, coapRequest);
-        coapRequest.addMessageObserver(observer);
-        pendingRequests.put(observer.getRequestKey(), coapRequest);
-    }
-
-    private void removePendingRequest(String key, Request coapRequest) {
-        Validate.notNull(key);
-        pendingRequests.remove(key, coapRequest);
-    }
-
-    private AtomicLong idGenerator = new AtomicLong(0l);
-
-    private class CleanerMessageObserver extends MessageObserverAdapter {
-
-        private final String requestKey;
-        private final Request coapRequest;
-
-        public CleanerMessageObserver(String registrationId, Request coapRequest) {
-            super();
-            requestKey = getKey(registrationId, idGenerator.incrementAndGet());
-            this.coapRequest = coapRequest;
-        }
-
-        public String getRequestKey() {
-            return requestKey;
-        }
-
-        @Override
-        public void onRetransmission() {
-        }
-
-        @Override
-        public void onResponse(Response response) {
-            removePendingRequest(requestKey, coapRequest);
-        }
-
-        @Override
-        public void onAcknowledgement() {
-            // We should remove the request on acknowledgement as we only want to avoid CoAP retransmission.
-            // But for transparent block-wise first request could be ACK and this does not mean that the next one should
-            // not be cancelled later.
-            // So waiting for a response, a cancel or a failure seems to be the only way.
-        }
-
-        @Override
-        protected void failed() {
-            removePendingRequest(requestKey, coapRequest);
-        }
-
-        @Override
-        public void onCancel() {
-            removePendingRequest(requestKey, coapRequest);
-        }
-    }
-
-    /**
-     * Gets the CoAP endpoint that should be used to communicate with a given client.
-     *
-     * @param registration the client
-     * @return the CoAP endpoint bound to the same network address and port that the client connected to during
-     *         registration. If no such CoAP endpoint is available, the first CoAP endpoint from the list of registered
-     *         endpoints is returned
-     */
-    private Endpoint getEndpointForClient(Registration registration) {
-        for (Endpoint ep : endpoints) {
-            InetSocketAddress endpointAddress = ep.getAddress();
-            if (endpointAddress.equals(registration.getRegistrationEndpointAddress())) {
-                return ep;
-            }
-        }
-        throw new IllegalStateException(
-                "can't find the client endpoint for address : " + registration.getRegistrationEndpointAddress());
+        sender.cancelRequests(registration.getId());
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
index 85242812..c3752abb 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/LwM2mResponseBuilder.java
@@ -57,9 +57,7 @@ import org.eclipse.leshan.core.response.ObserveResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
 import org.eclipse.leshan.core.response.WriteAttributesResponse;
 import org.eclipse.leshan.core.response.WriteResponse;
-import org.eclipse.leshan.server.californium.observation.ObservationServiceImpl;
 import org.eclipse.leshan.server.californium.observation.ObserveUtil;
-import org.eclipse.leshan.server.registration.Registration;
 import org.eclipse.leshan.util.Hex;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -71,17 +69,15 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
     private LwM2mResponse lwM2mresponse;
     private final Request coapRequest;
     private final Response coapResponse;
-    private final ObservationServiceImpl observationService;
-    private final Registration registration;
+    private final String clientEndpoint;
     private final LwM2mModel model;
     private final LwM2mNodeDecoder decoder;
 
-    public LwM2mResponseBuilder(Request coapRequest, Response coapResponse, Registration registration, LwM2mModel model,
-            ObservationServiceImpl observationService, LwM2mNodeDecoder decoder) {
+    public LwM2mResponseBuilder(Request coapRequest, Response coapResponse, String clientEndpoint, LwM2mModel model,
+            LwM2mNodeDecoder decoder) {
         this.coapRequest = coapRequest;
         this.coapResponse = coapResponse;
-        this.observationService = observationService;
-        this.registration = registration;
+        this.clientEndpoint = clientEndpoint;
         this.model = model;
         this.decoder = decoder;
     }
@@ -94,12 +90,11 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
                     coapResponse.getPayloadString(), coapResponse);
         } else if (coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT) {
             // handle success response:
-            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request,
-                    registration.getEndpoint());
+            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint);
             lwM2mresponse = new ReadResponse(ResponseCode.CONTENT, content, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -114,7 +109,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             Link[] links;
             if (MediaTypeRegistry.APPLICATION_LINK_FORMAT != coapResponse.getOptions().getContentFormat()) {
                 LOG.debug("Expected LWM2M Client [{}] to return application/link-format [{}] content but got [{}]",
-                        registration.getEndpoint(), MediaTypeRegistry.APPLICATION_LINK_FORMAT,
+                        clientEndpoint, MediaTypeRegistry.APPLICATION_LINK_FORMAT,
                         coapResponse.getOptions().getContentFormat());
                 links = new Link[] {}; // empty list
             } else {
@@ -123,7 +118,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new DiscoverResponse(ResponseCode.CONTENT, links, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -138,7 +133,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new WriteResponse(ResponseCode.CHANGED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -153,7 +148,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new WriteAttributesResponse(ResponseCode.CHANGED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -168,7 +163,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new ExecuteResponse(ResponseCode.CHANGED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -184,7 +179,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
                     null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -199,7 +194,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new DeleteResponse(ResponseCode.DELETED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -213,12 +208,10 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
                 // This is for backward compatibility, when the spec say notification used CHANGED code
                 || coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) {
             // handle success response:
-            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request,
-                    registration.getEndpoint());
+            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint);
             if (coapResponse.getOptions().hasObserve()) {
                 // observe request successful
                 Observation observation = ObserveUtil.createLwM2mObservation(coapRequest);
-                observationService.addObservation(registration, observation);
                 // add the observation to an ObserveResponse instance
                 lwM2mresponse = new ObserveResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null,
                         observation, null, coapResponse);
@@ -228,7 +221,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             }
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -242,22 +235,12 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
                 // This is for backward compatibility, when the spec say notification used CHANGED code
                 || coapResponse.getCode() == org.eclipse.californium.core.coap.CoAP.ResponseCode.CHANGED) {
             // handle success response:
-            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request,
-                    registration.getEndpoint());
-            if (coapResponse.getOptions().hasObserve()) {
-                // observe request successful
-                Observation observation = ObserveUtil.createLwM2mObservation(coapRequest);
-                observationService.addObservation(registration, observation);
-                // add the observation to an ObserveResponse instance
-                lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content,
-                        null, observation, null, coapResponse);
-            } else {
-                lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content,
-                        null, null, null, coapResponse);
-            }
+            LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse, request, clientEndpoint);
+            lwM2mresponse = new CancelObservationResponse(toLwM2mResponseCode(coapResponse.getCode()), content, null,
+                    null, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -272,7 +255,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new BootstrapWriteResponse(ResponseCode.CHANGED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -287,7 +270,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new BootstrapDeleteResponse(ResponseCode.DELETED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
@@ -302,7 +285,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             lwM2mresponse = new BootstrapFinishResponse(ResponseCode.CHANGED, null, coapResponse);
         } else {
             // handle unexpected response:
-            handleUnexpectedResponseCode(registration.getEndpoint(), request, coapResponse);
+            handleUnexpectedResponseCode(clientEndpoint, request, coapResponse);
         }
     }
 
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java
new file mode 100644
index 00000000..6f39a6ab
--- /dev/null
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/RequestSender.java
@@ -0,0 +1,263 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.californium.request;
+
+import java.util.SortedMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.californium.core.coap.MessageObserver;
+import org.eclipse.californium.core.coap.MessageObserverAdapter;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.californium.elements.EndpointContext;
+import org.eclipse.leshan.core.californium.AsyncRequestObserver;
+import org.eclipse.leshan.core.californium.CoapAsyncRequestObserver;
+import org.eclipse.leshan.core.californium.CoapResponseCallback;
+import org.eclipse.leshan.core.californium.CoapSyncRequestObserver;
+import org.eclipse.leshan.core.californium.EndpointContextUtil;
+import org.eclipse.leshan.core.californium.SyncRequestObserver;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
+import org.eclipse.leshan.core.request.DownlinkRequest;
+import org.eclipse.leshan.core.request.Identity;
+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.server.californium.bootstrap.CaliforniumLwM2mBootstrapRequestSender;
+import org.eclipse.leshan.util.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This sender is able to send LWM2M or CoAP request in a synchronous or asynchronous way.
+ * <p>
+ * It can also link requests to a kind of "session" and cancel all ongoing requests associated to a given "session".
+ *
+ */
+public class RequestSender {
+    static final Logger LOG = LoggerFactory.getLogger(CaliforniumLwM2mBootstrapRequestSender.class);
+
+    private final Endpoint nonSecureEndpoint;
+    private final Endpoint secureEndpoint;
+    private final LwM2mNodeDecoder decoder;
+    private final LwM2mNodeEncoder encoder;
+
+    // A map which contains all ongoing CoAP requests
+    // This is used to be able to cancel request
+    private final ConcurrentNavigableMap<String/* sessionId#requestId */, Request /* ongoing coap Request */> ongoingRequests = new ConcurrentSkipListMap<>();
+
+    public RequestSender(Endpoint secureEndpoint, Endpoint nonSecureEndpoint, LwM2mNodeEncoder encoder,
+            LwM2mNodeDecoder decoder) {
+        this.secureEndpoint = secureEndpoint;
+        this.nonSecureEndpoint = nonSecureEndpoint;
+        this.encoder = encoder;
+        this.decoder = decoder;
+    }
+
+    public <T extends LwM2mResponse> T sendLwm2mRequest(final String endpointName, Identity destination,
+            String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request, long timeout)
+            throws InterruptedException {
+
+        // Create the CoAP request from LwM2m request
+        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, rootPath, sessionId,
+                endpointName, model, encoder);
+        request.accept(coapClientRequestBuilder);
+        final Request coapRequest = coapClientRequestBuilder.getRequest();
+
+        // Send CoAP request synchronously
+        SyncRequestObserver<T> syncMessageObserver = new SyncRequestObserver<T>(coapRequest, timeout) {
+            @Override
+            public T buildResponse(Response coapResponse) {
+                // Build LwM2m response
+                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
+                        endpointName, model, decoder);
+                request.accept(lwm2mResponseBuilder);
+                return lwm2mResponseBuilder.getResponse();
+            }
+        };
+        coapRequest.addMessageObserver(syncMessageObserver);
+
+        // Store pending request to be able to cancel it later
+        addOngoingRequest(sessionId, coapRequest);
+
+        // Send CoAP request asynchronously
+        if (destination.isSecure())
+            secureEndpoint.sendRequest(coapRequest);
+        else
+            nonSecureEndpoint.sendRequest(coapRequest);
+
+        // Wait for response, then return it
+        return syncMessageObserver.waitForResponse();
+    }
+
+    public <T extends LwM2mResponse> void sendLwm2mRequest(final String endpointName, Identity destination,
+            String sessionId, final LwM2mModel model, String rootPath, final DownlinkRequest<T> request, long timeout,
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
+        // Create the CoAP request from LwM2m request
+        CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(destination, rootPath, sessionId,
+                endpointName, model, encoder);
+        request.accept(coapClientRequestBuilder);
+        final Request coapRequest = coapClientRequestBuilder.getRequest();
+
+        // Add CoAP request callback
+        MessageObserver obs = new AsyncRequestObserver<T>(coapRequest, responseCallback, errorCallback, timeout) {
+            @Override
+            public T buildResponse(Response coapResponse) {
+                // Build LwM2m response
+                LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<>(coapRequest, coapResponse,
+                        endpointName, model, decoder);
+                request.accept(lwm2mResponseBuilder);
+                return lwm2mResponseBuilder.getResponse();
+            }
+        };
+        coapRequest.addMessageObserver(obs);
+
+        // Store pending request to be able to cancel it later
+        addOngoingRequest(sessionId, coapRequest);
+
+        // Send CoAP request asynchronously
+        if (destination.isSecure())
+            secureEndpoint.sendRequest(coapRequest);
+        else
+            nonSecureEndpoint.sendRequest(coapRequest);
+    }
+
+    public Response sendCoapRequest(Identity destination, String sessionId, Request coapRequest, long timeout)
+            throws InterruptedException {
+
+        // Define destination
+        EndpointContext context = EndpointContextUtil.extractContext(destination);
+        coapRequest.setDestinationContext(context);
+
+        // Send CoAP request synchronously
+        CoapSyncRequestObserver syncMessageObserver = new CoapSyncRequestObserver(coapRequest, timeout);
+        coapRequest.addMessageObserver(syncMessageObserver);
+
+        // Store pending request to be able to cancel it later
+        addOngoingRequest(sessionId, coapRequest);
+
+        // Send CoAP request asynchronously
+        if (destination.isSecure())
+            secureEndpoint.sendRequest(coapRequest);
+        else
+            nonSecureEndpoint.sendRequest(coapRequest);
+
+        // Wait for response, then return it
+        return syncMessageObserver.waitForCoapResponse();
+    }
+
+    public void sendCoapRequest(Identity destination, String sessionId, Request coapRequest, long timeout,
+            CoapResponseCallback responseCallback, ErrorCallback errorCallback) {
+
+        // Define destination
+        EndpointContext context = EndpointContextUtil.extractContext(destination);
+        coapRequest.setDestinationContext(context);
+
+        // Add CoAP request callback
+        MessageObserver obs = new CoapAsyncRequestObserver(coapRequest, responseCallback, errorCallback, timeout);
+        coapRequest.addMessageObserver(obs);
+
+        // Store pending request to be able to cancel it later
+        addOngoingRequest(sessionId, coapRequest);
+
+        // Send CoAP request asynchronously
+        if (destination.isSecure())
+            secureEndpoint.sendRequest(coapRequest);
+        else
+            nonSecureEndpoint.sendRequest(coapRequest);
+    }
+
+    public void cancelRequests(String sessionID) {
+        Validate.notNull(sessionID);
+        SortedMap<String, Request> requests = ongoingRequests.subMap(getFloorKey(sessionID), getCeilingKey(sessionID));
+        for (Request coapRequest : requests.values()) {
+            coapRequest.cancel();
+        }
+        requests.clear();
+    }
+
+    private static String getFloorKey(String sessionID) {
+        // The key format is sessionid#long, So we need a key which is always before this pattern (in natural order).
+        return sessionID + '#';
+    }
+
+    private static String getCeilingKey(String sessionID) {
+        // The key format is sessionid#long, So we need a key which is always after this pattern (in natural order).
+        return sessionID + "#A";
+    }
+
+    private static String getKey(String sessionID, long requestId) {
+        return sessionID + '#' + requestId;
+    }
+
+    private void addOngoingRequest(String sessionID, Request coapRequest) {
+        if (sessionID != null) {
+            CleanerMessageObserver observer = new CleanerMessageObserver(sessionID, coapRequest);
+            coapRequest.addMessageObserver(observer);
+            ongoingRequests.put(observer.getRequestKey(), coapRequest);
+        }
+    }
+
+    private void removeOngoingRequest(String key, Request coapRequest) {
+        Validate.notNull(key);
+        ongoingRequests.remove(key, coapRequest);
+    }
+
+    private AtomicLong idGenerator = new AtomicLong(0l);
+
+    private class CleanerMessageObserver extends MessageObserverAdapter {
+
+        private final String requestKey;
+        private final Request coapRequest;
+
+        public CleanerMessageObserver(String sessionID, Request coapRequest) {
+            super();
+            requestKey = getKey(sessionID, idGenerator.incrementAndGet());
+            this.coapRequest = coapRequest;
+        }
+
+        public String getRequestKey() {
+            return requestKey;
+        }
+
+        @Override
+        public void onRetransmission() {
+        }
+
+        @Override
+        public void onResponse(Response response) {
+            removeOngoingRequest(requestKey, coapRequest);
+        }
+
+        @Override
+        public void onAcknowledgement() {
+        }
+
+        @Override
+        protected void failed() {
+            removeOngoingRequest(requestKey, coapRequest);
+        }
+
+        @Override
+        public void onCancel() {
+            removeOngoingRequest(requestKey, coapRequest);
+        }
+    }
+}
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 01e2b032..dd7b60fd 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,10 +23,6 @@ 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
      */
@@ -51,6 +47,10 @@ public enum BootstrapFailureCause {
      * 'Bootstrap Finish' failed
      */
     FINISH_FAILED,
+    /**
+     * The bootstrap session is cancelled, generally because device starts a new one.
+     */
+    CANCELLED,
     /**
      * An unexpected error occured
      */
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 9921690a..a0fb7b12 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
@@ -26,6 +26,11 @@ import org.eclipse.leshan.core.request.Identity;
  */
 public interface BootstrapSession {
 
+    /**
+     * @Return the identifier for this session
+     */
+    String getId();
+
     /**
      * @return the endpoint of the LwM2M client.
      */
@@ -37,7 +42,7 @@ public interface BootstrapSession {
     Identity getIdentity();
 
     /**
-     * @return true if the LwM2M client is authorized to start a bootstrap session.
+     * @return <code>true</code> if the LwM2M client is authorized to start a bootstrap session.
      */
     boolean isAuthorized();
 
@@ -52,4 +57,13 @@ public interface BootstrapSession {
      */
     long getCreationTime();
 
+    /**
+     * Cancel the current session
+     */
+    void cancel();
+
+    /**
+     * @return True if this session was cancellled
+     */
+    boolean isCancelled();
 }
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 40ddc7ed..7bc29072 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,8 +15,8 @@
  *******************************************************************************/
 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;
 
 /**
@@ -70,7 +70,7 @@ public interface BootstrapSessionManager {
      * @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);
+    public void onResponseSuccess(BootstrapSession bsSession, DownlinkRequest<? extends LwM2mResponse> request);
 
     /**
      * Called when we receive a error response to a request.
@@ -81,7 +81,7 @@ public interface BootstrapSessionManager {
      * 
      * @return a {@link BootstrapPolicy} given the way to continue the bootstrap session.
      */
-    public BootstrapPolicy onResponseError(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+    public BootstrapPolicy onResponseError(BootstrapSession bsSession, DownlinkRequest<? extends LwM2mResponse> request,
             LwM2mResponse response);
 
     /**
@@ -93,8 +93,8 @@ public interface BootstrapSessionManager {
      * 
      * @return a {@link BootstrapPolicy} given the way to continue the bootstrap session.
      */
-    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
-            Throwable cause);
+    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession,
+            DownlinkRequest<? extends LwM2mResponse> request, Throwable cause);
 
     /**
      * Performs any housekeeping related to the successful ending of a Bootstrapping session.
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 1946f2e9..474b6c53 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
@@ -57,32 +57,25 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
     // send a Confirmable message to the time when an acknowledgement is no longer expected.
     public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms
 
-    public static final long DEFAULT_LIFETIME = 15 * 60 * 1000l; // 15 minutes
-
     protected final BootstrapConfigStore store;
 
     protected final LwM2mBootstrapRequestSender sender;
     protected final long requestTimeout;
 
-    // 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) {
-        this(store, sender, sessionManager, DEFAULT_TIMEOUT, DEFAULT_LIFETIME);
+        this(store, sender, sessionManager, DEFAULT_TIMEOUT);
     }
 
     public DefaultBootstrapHandler(BootstrapConfigStore store, LwM2mBootstrapRequestSender sender,
-            BootstrapSessionManager sessionManager, long requestTimeout, long sessionLifetime) {
+            BootstrapSessionManager sessionManager, long requestTimeout) {
         this.store = store;
         this.sender = sender;
         this.sessionManager = sessionManager;
         this.requestTimeout = requestTimeout;
-        this.sessionLifeTime = sessionLifetime;
     }
 
     @Override
@@ -98,21 +91,14 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         }
 
         // 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"));
-                }
+        BootstrapSession oldSession = onGoingSession.put(endpoint, session);
+        if (oldSession != null) {
+            // stop previous ongoing session.
+            synchronized (oldSession) {
+                oldSession.cancel();
+                this.sender.cancelOngoingRequests(oldSession);
             }
-        } while (oldSession != null);
+        }
 
         try {
             // Get the desired bootstrap config for the endpoint
@@ -145,7 +131,9 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected void stopSession(BootstrapSession session, BootstrapFailureCause cause) {
         if (!onGoingSession.remove(session.getEndpoint(), session)) {
-            LOG.warn("{} was already removed", session);
+            if (!session.isCancelled()) {
+                LOG.warn("{} was already removed", session);
+            }
         }
         // if there is no cause of failure, this is a success
         if (cause == null) {
@@ -191,6 +179,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected void afterDelete(BootstrapSession session, BootstrapConfig cfg, List<String> pathToDelete,
             BootstrapPolicy policy) {
+        if (session.isCancelled()) {
+            stopSession(session, CANCELLED);
+            return;
+        }
         switch (policy) {
         case CONTINUE:
             pathToDelete.remove(0);
@@ -258,6 +250,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected void afterWriteSecurities(BootstrapSession session, BootstrapConfig cfg,
             List<Integer> securityInstancesToWrite, BootstrapPolicy policy) {
+        if (session.isCancelled()) {
+            stopSession(session, CANCELLED);
+            return;
+        }
         switch (policy) {
         case CONTINUE:
             securityInstancesToWrite.remove(0);
@@ -324,6 +320,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected void afterWriteServers(BootstrapSession session, BootstrapConfig cfg,
             List<Integer> serverInstancesToWrite, BootstrapPolicy policy) {
+        if (session.isCancelled()) {
+            stopSession(session, CANCELLED);
+            return;
+        }
         switch (policy) {
         case CONTINUE:
             serverInstancesToWrite.remove(0);
@@ -389,6 +389,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected void afterWritedAcls(BootstrapSession session, BootstrapConfig cfg, List<Integer> aclInstancesToWrite,
             BootstrapPolicy policy) {
+        if (session.isCancelled()) {
+            stopSession(session, CANCELLED);
+            return;
+        }
         switch (policy) {
         case CONTINUE:
             aclInstancesToWrite.remove(0);
@@ -438,6 +442,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
     }
 
     protected void afterBootstrapFinished(BootstrapSession session, BootstrapConfig cfg, BootstrapPolicy policy) {
+        if (session.isCancelled()) {
+            stopSession(session, CANCELLED);
+            return;
+        }
         switch (policy) {
         case CONTINUE:
             stopSession(session, null);
@@ -461,8 +469,13 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     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);
+        synchronized (session) {
+            if (!session.isCancelled()) {
+                sender.send(session, request, requestTimeout, responseCallback, errorCallback);
+            } else {
+                stopSession(session, CANCELLED);
+            }
+        }
     }
 
     protected abstract class SafeResponseCallback<T extends LwM2mResponse> implements ResponseCallback<T> {
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
index 64e1aad4..2d1e9275 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSession.java
@@ -17,18 +17,22 @@ package org.eclipse.leshan.server.bootstrap;
 
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.util.RandomStringUtils;
 
 /**
  * A default implementation for {@link BootstrapSession}
  */
 public class DefaultBootstrapSession implements BootstrapSession {
 
+    private final String id;
     private final String endpoint;
     private final Identity identity;
     private final boolean authorized;
     private final ContentFormat contentFormat;
     private final long creationTime;
 
+    private volatile boolean cancelled = false;
+
     /**
      * Create a {@link DefaultBootstrapSession} using default {@link ContentFormat#TLV} content format and using
      * <code>System.currentTimeMillis()</code> to set the creation time.
@@ -65,6 +69,7 @@ public class DefaultBootstrapSession implements BootstrapSession {
      */
     public DefaultBootstrapSession(String endpoint, Identity identity, boolean authorized, ContentFormat contentFormat,
             long creationTime) {
+        this.id = RandomStringUtils.random(10, true, true);
         this.endpoint = endpoint;
         this.identity = identity;
         this.authorized = authorized;
@@ -72,6 +77,11 @@ public class DefaultBootstrapSession implements BootstrapSession {
         this.creationTime = creationTime;
     }
 
+    @Override
+    public String getId() {
+        return id;
+    }
+
     @Override
     public String getEndpoint() {
         return endpoint;
@@ -97,11 +107,20 @@ public class DefaultBootstrapSession implements BootstrapSession {
         return creationTime;
     }
 
+    @Override
+    public void cancel() {
+        cancelled = true;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
     @Override
     public String toString() {
         return String.format(
-                "BootstrapSession [endpoint=%s, identity=%s, authorized=%s, contentFormat=%s, creationTime=%dms]",
-                endpoint, identity, authorized, contentFormat, creationTime);
+                "BootstrapSession [id=%s, endpoint=%s, identity=%s, authorized=%s, contentFormat=%s, creationTime=%dms, cancelled=%s]",
+                id, endpoint, identity, authorized, contentFormat, creationTime, cancelled);
     }
-
-}
+}
\ No newline at end of file
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSessionManager.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSessionManager.java
index 0bceea03..ca2ba2a3 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSessionManager.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapSessionManager.java
@@ -18,12 +18,14 @@ package org.eclipse.leshan.server.bootstrap;
 import java.util.List;
 
 import org.eclipse.leshan.core.request.BootstrapFinishRequest;
+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.security.BootstrapSecurityStore;
 import org.eclipse.leshan.server.security.SecurityChecker;
 import org.eclipse.leshan.server.security.SecurityInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Implementation of a session manager.
@@ -34,6 +36,8 @@ import org.eclipse.leshan.server.security.SecurityInfo;
  */
 public class DefaultBootstrapSessionManager implements BootstrapSessionManager {
 
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultBootstrapSessionManager.class);
+
     private BootstrapSecurityStore bsSecurityStore;
     private SecurityChecker securityChecker;
 
@@ -67,25 +71,35 @@ public class DefaultBootstrapSessionManager implements BootstrapSessionManager {
         } else {
             authorized = true;
         }
-
-        return new DefaultBootstrapSession(endpoint, clientIdentity, authorized);
+        DefaultBootstrapSession session = new DefaultBootstrapSession(endpoint, clientIdentity, authorized);
+        LOG.trace("Bootstrap session started : {}", session);
+        return session;
     }
 
     @Override
     public void end(BootstrapSession bsSession) {
+        LOG.trace("Bootstrap session finished : {}", bsSession);
     }
 
     @Override
     public void failed(BootstrapSession bsSession, BootstrapFailureCause cause) {
+        LOG.trace("Bootstrap session failed by {}: {}", cause, bsSession);
     }
 
     @Override
-    public void onResponseSuccess(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request) {
+    public void onResponseSuccess(BootstrapSession bsSession, DownlinkRequest<? extends LwM2mResponse> request) {
+        if (LOG.isTraceEnabled())
+            LOG.trace("{} {} receives success response for {} : {}", request.getClass().getSimpleName(),
+                    request.getPath(), bsSession, request);
     }
 
     @Override
-    public BootstrapPolicy onResponseError(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
+    public BootstrapPolicy onResponseError(BootstrapSession bsSession, DownlinkRequest<? extends LwM2mResponse> request,
             LwM2mResponse response) {
+        if (LOG.isTraceEnabled())
+            LOG.trace("{} {} receives error response {} for {} : {}", request.getClass().getSimpleName(),
+                    request.getPath(), response, bsSession, request);
+
         if (request instanceof BootstrapFinishRequest) {
             return BootstrapPolicy.STOP;
         }
@@ -93,8 +107,12 @@ public class DefaultBootstrapSessionManager implements BootstrapSessionManager {
     }
 
     @Override
-    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request,
-            Throwable cause) {
+    public BootstrapPolicy onRequestFailure(BootstrapSession bsSession,
+            DownlinkRequest<? extends LwM2mResponse> request, Throwable cause) {
+        if (LOG.isTraceEnabled())
+            LOG.trace("{} {} failed because of {} for {} : {}", request.getClass().getSimpleName(), request.getPath(),
+                    cause, bsSession, request);
+
         return BootstrapPolicy.STOP;
     }
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/LwM2mBootstrapRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/LwM2mBootstrapRequestSender.java
index 1c5bb25c..ab3e93f0 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/LwM2mBootstrapRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/LwM2mBootstrapRequestSender.java
@@ -16,7 +16,6 @@
 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.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
@@ -28,12 +27,19 @@ public interface LwM2mBootstrapRequestSender {
      * @return the LWM2M response. The response can be <code>null</code> if the timeout (given parameter or CoAP
      *         timeout) expires.
      */
-    <T extends LwM2mResponse> T send(String clientEndpoint, Identity client, DownlinkRequest<T> request, long timeout)
+    <T extends LwM2mResponse> T send(BootstrapSession destination, DownlinkRequest<T> request, long timeout)
             throws InterruptedException;
 
     /**
      * Send a Lightweight M2M request asynchronously.
      */
-    <T extends LwM2mResponse> void send(String clientEndpoint, Identity client, DownlinkRequest<T> request,
-            long timeout, ResponseCallback<T> responseCallback, ErrorCallback errorCallback);
+    <T extends LwM2mResponse> void send(BootstrapSession destination, DownlinkRequest<T> request, long timeout,
+            ResponseCallback<T> responseCallback, ErrorCallback errorCallback);
+
+    /**
+     * cancel all ongoing requests for a given bootstrap session.
+     * 
+     * @param session the bootstrap session for which we need to cancel requests.
+     */
+    void cancelOngoingRequests(BootstrapSession session);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
index e09c06ab..3996eca2 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/QueueModeLwM2mRequestSender.java
@@ -117,7 +117,7 @@ public class QueueModeLwM2mRequestSender implements LwM2mRequestSender {
     }
 
     @Override
-    public void cancelPendingRequests(Registration registration) {
-        delegatedSender.cancelPendingRequests(registration);
+    public void cancelOngoingRequests(Registration registration) {
+        delegatedSender.cancelOngoingRequests(registration);
     }
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
index afd17b79..5dfb1e92 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java
@@ -52,11 +52,6 @@ public class Registration implements Serializable {
 
     private final Identity identity;
 
-    /*
-     * The address of the LWM2M Server's CoAP end point the client used to register.
-     */
-    private final InetSocketAddress registrationEndpointAddress;
-
     private final long lifeTimeInSec;
 
     private final String smsNumber;
@@ -86,20 +81,17 @@ public class Registration implements Serializable {
     private final Date lastUpdate;
 
     protected Registration(String id, String endpoint, Identity identity, String lwM2mVersion, Long lifetimeInSec,
-            String smsNumber, BindingMode bindingMode, Link[] objectLinks,
-            InetSocketAddress registrationEndpointAddress, Date registrationDate, Date lastUpdate,
+            String smsNumber, BindingMode bindingMode, Link[] objectLinks, Date registrationDate, Date lastUpdate,
             Map<String, String> additionalRegistrationAttributes, Map<Integer, String> supportedObjects) {
 
         Validate.notNull(id);
         Validate.notEmpty(endpoint);
         Validate.notNull(identity);
-        Validate.notNull(registrationEndpointAddress);
 
         this.id = id;
         this.identity = identity;
         this.endpoint = endpoint;
         this.smsNumber = smsNumber;
-        this.registrationEndpointAddress = registrationEndpointAddress;
 
         this.objectLinks = objectLinks;
         // Parse object link to extract root path.
@@ -175,23 +167,6 @@ public class Registration implements Serializable {
         return identity.getPeerAddress().getPort();
     }
 
-    /**
-     * Gets the network address and port number of LWM2M Server's CoAP endpoint the client originally registered at.
-     * 
-     * A LWM2M Server may listen on multiple CoAP end points, e.g. a non-secure and a secure one. Clients are often
-     * behind a firewall which will only let incoming UDP packets pass if they originate from the same address:port that
-     * the client has initiated communication with, e.g. by means of registering with the LWM2M Server. It is therefore
-     * important to know, which of the server's CoAP end points the client contacted for registration.
-     * 
-     * This information can be used to uniquely identify the CoAP endpoint that should be used to access resources on
-     * the client.
-     * 
-     * @return the network address and port number
-     */
-    public InetSocketAddress getRegistrationEndpointAddress() {
-        return registrationEndpointAddress;
-    }
-
     public Link[] getObjectLinks() {
         return objectLinks;
     }
@@ -338,9 +313,9 @@ public class Registration implements Serializable {
     @Override
     public String toString() {
         return String.format(
-                "Registration [registrationDate=%s, identity=%s, registrationEndpoint=%s, lifeTimeInSec=%s, smsNumber=%s, lwM2mVersion=%s, bindingMode=%s, endpoint=%s, registrationId=%s, objectLinks=%s, lastUpdate=%s]",
-                registrationDate, identity, registrationEndpointAddress, lifeTimeInSec, smsNumber, lwM2mVersion,
-                bindingMode, endpoint, id, Arrays.toString(objectLinks), lastUpdate);
+                "Registration [registrationDate=%s, identity=%s, lifeTimeInSec=%s, smsNumber=%s, lwM2mVersion=%s, bindingMode=%s, endpoint=%s, registrationId=%s, objectLinks=%s, lastUpdate=%s]",
+                registrationDate, identity, lifeTimeInSec, smsNumber, lwM2mVersion, bindingMode, endpoint, id,
+                Arrays.toString(objectLinks), lastUpdate);
     }
 
     /**
@@ -413,7 +388,6 @@ public class Registration implements Serializable {
         private final String registrationId;
         private final String endpoint;
         private final Identity identity;
-        private final InetSocketAddress registrationEndpointAddress;
 
         private Date registrationDate;
         private Date lastUpdate;
@@ -425,18 +399,14 @@ public class Registration implements Serializable {
         private Map<Integer, String> supportedObjects;
         private Map<String, String> additionalRegistrationAttributes;
 
-        public Builder(String registrationId, String endpoint, Identity identity,
-                InetSocketAddress registrationEndpointAddress) {
+        public Builder(String registrationId, String endpoint, Identity identity) {
 
             Validate.notNull(registrationId);
             Validate.notEmpty(endpoint);
             Validate.notNull(identity);
-            Validate.notNull(registrationEndpointAddress);
             this.registrationId = registrationId;
             this.endpoint = endpoint;
             this.identity = identity;
-            this.registrationEndpointAddress = registrationEndpointAddress;
-
         }
 
         public Builder registrationDate(Date registrationDate) {
@@ -487,8 +457,8 @@ public class Registration implements Serializable {
         public Registration build() {
             return new Registration(Builder.this.registrationId, Builder.this.endpoint, Builder.this.identity,
                     Builder.this.lwM2mVersion, Builder.this.lifeTimeInSec, Builder.this.smsNumber, this.bindingMode,
-                    this.objectLinks, this.registrationEndpointAddress, this.registrationDate, this.lastUpdate,
-                    this.additionalRegistrationAttributes, this.supportedObjects);
+                    this.objectLinks, this.registrationDate, this.lastUpdate, this.additionalRegistrationAttributes,
+                    this.supportedObjects);
         }
 
     }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationHandler.java
index 2c7a6958..bb2a902c 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationHandler.java
@@ -17,7 +17,6 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.registration;
 
-import java.net.InetSocketAddress;
 import java.util.Date;
 
 import org.eclipse.leshan.core.request.DeregisterRequest;
@@ -51,12 +50,10 @@ public class RegistrationHandler {
         this.registrationIdProvider = registrationIdProvider;
     }
 
-    public SendableResponse<RegisterResponse> register(Identity sender, RegisterRequest registerRequest,
-            InetSocketAddress serverEndpoint) {
+    public SendableResponse<RegisterResponse> register(Identity sender, RegisterRequest registerRequest) {
 
         Registration.Builder builder = new Registration.Builder(
-                registrationIdProvider.getRegistrationId(registerRequest), registerRequest.getEndpointName(), sender,
-                serverEndpoint);
+                registrationIdProvider.getRegistrationId(registerRequest), registerRequest.getEndpointName(), sender);
 
         builder.lwM2mVersion(registerRequest.getLwVersion()).lifeTimeInSec(registerRequest.getLifetime())
                 .bindingMode(registerRequest.getBindingMode()).objectLinks(registerRequest.getObjectLinks())
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationUpdate.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationUpdate.java
index 474284c3..92bbd7a0 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationUpdate.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/RegistrationUpdate.java
@@ -80,7 +80,7 @@ public class RegistrationUpdate {
         Date lastUpdate = new Date();
 
         Registration.Builder builder = new Registration.Builder(registration.getId(), registration.getEndpoint(),
-                identity, registration.getRegistrationEndpointAddress());
+                identity);
 
         builder.lwM2mVersion(registration.getLwM2mVersion()).lifeTimeInSec(lifeTimeInSec).smsNumber(smsNumber)
                 .bindingMode(bindingMode).objectLinks(linkObject).registrationDate(registration.getRegistrationDate())
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
index 8cc5961d..23df6e49 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/request/LwM2mRequestSender.java
@@ -18,7 +18,6 @@ package org.eclipse.leshan.server.request;
 
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.request.DownlinkRequest;
-import org.eclipse.leshan.core.request.exception.RequestCanceledException;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
@@ -55,11 +54,10 @@ public interface LwM2mRequestSender {
             ResponseCallback<T> responseCallback, ErrorCallback errorCallback);
 
     /**
-     * cancel all pending messages for a LWM2M client identified by the registration identifier. In case a client
-     * de-registers, the consumer can use this method to cancel all messages pending for the given client.
+     * cancel all ongoing messages for a LWM2M client identified by the registration identifier. In case a client
+     * de-registers, the consumer can use this method to cancel all ongoing messages for the given client.
      * 
      * @param registration client registration meta data of a LWM2M client.
-     * @throws RequestCanceledException when a request is already being sent in CoAP, then the exception is thrown.
      */
-    void cancelPendingRequests(Registration registration);
+    void cancelOngoingRequests(Registration registration);
 }
diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java
index 7eb467ae..b189fbe1 100644
--- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java
+++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java
@@ -15,7 +15,6 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.redis.serialization;
 
-import java.net.InetSocketAddress;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -38,8 +37,6 @@ public class RegistrationSerDes {
         JsonObject o = Json.object();
         o.add("regDate", r.getRegistrationDate().getTime());
         o.add("identity", IdentitySerDes.serialize(r.getIdentity()));
-        o.add("regAddr", r.getRegistrationEndpointAddress().getHostString());
-        o.add("regPort", r.getRegistrationEndpointAddress().getPort());
         o.add("lt", r.getLifeTimeInSec());
         if (r.getSmsNumber() != null) {
             o.add("sms", r.getSmsNumber());
@@ -87,8 +84,7 @@ public class RegistrationSerDes {
 
     public static Registration deserialize(JsonObject jObj) {
         Registration.Builder b = new Registration.Builder(jObj.getString("regId", null), jObj.getString("ep", null),
-                IdentitySerDes.deserialize(jObj.get("identity").asObject()),
-                new InetSocketAddress(jObj.getString("regAddr", null), jObj.getInt("regPort", 0)));
+                IdentitySerDes.deserialize(jObj.get("identity").asObject()));
         b.bindingMode(BindingMode.valueOf(jObj.getString("bnd", null)));
         b.lastUpdate(new Date(jObj.getLong("lastUp", 0)));
         b.lifeTimeInSec(jObj.getLong("lt", 0));


Test patch:
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumTestSupport.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumTestSupport.java
index ce508666..458cca1e 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumTestSupport.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumTestSupport.java
@@ -37,7 +37,7 @@ public class CaliforniumTestSupport {
         registrationAddress = InetSocketAddress.createUnresolved("localhost", LwM2m.DEFAULT_COAP_PORT);
 
         Registration.Builder builder = new Registration.Builder("ID", "urn:client",
-                Identity.unsecure(Inet4Address.getLoopbackAddress(), 1000), registrationAddress);
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 1000));
 
         registration = builder.build();
     }
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
index b3c2e58e..20a8e92d 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
@@ -18,7 +18,6 @@ package org.eclipse.leshan.server.californium.impl;
 import static org.junit.Assert.*;
 
 import java.net.Inet4Address;
-import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
@@ -77,7 +76,7 @@ public class CoapRequestBuilderTest {
 
     private Registration newRegistration(String rootpath) throws UnknownHostException {
         Builder b = new Registration.Builder("regid", "endpoint",
-                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354), new InetSocketAddress(0));
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354));
         if (rootpath != null) {
             Map<String, String> attr = new HashMap<>();
             attr.put("rt", "oma.lwm2m");
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/InMemoryRegistrationStoreTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/InMemoryRegistrationStoreTest.java
index 0175baab..b8c31046 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/InMemoryRegistrationStoreTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/InMemoryRegistrationStoreTest.java
@@ -16,11 +16,9 @@
 package org.eclipse.leshan.server.californium.impl;
 
 import java.net.InetAddress;
-import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 
 import org.eclipse.leshan.Link;
-import org.eclipse.leshan.LwM2m;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.request.Identity;
 import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore;
@@ -95,8 +93,7 @@ public class InMemoryRegistrationStoreTest {
 
     private void givenASimpleRegistration(Long lifetime) {
 
-        Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port),
-                InetSocketAddress.createUnresolved("localhost", LwM2m.DEFAULT_COAP_PORT));
+        Registration.Builder builder = new Registration.Builder(registrationId, ep, Identity.unsecure(address, port));
 
         registration = builder.lifeTimeInSec(lifetime).smsNumber(sms).bindingMode(binding).objectLinks(objectLinks)
                 .build();
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/ObservationServiceTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/ObservationServiceTest.java
index 42ef5276..dd019699 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/ObservationServiceTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/ObservationServiceTest.java
@@ -16,13 +16,11 @@
 package org.eclipse.leshan.server.californium.impl;
 
 import java.net.InetAddress;
-import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.californium.core.coap.Request;
-import org.eclipse.leshan.LwM2m;
 import org.eclipse.leshan.core.californium.EndpointContextUtil;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
@@ -157,12 +155,10 @@ public class ObservationServiceTest {
     }
 
     public Registration givenASimpleClient(String registrationId) {
-        InetSocketAddress registrationAddress = InetSocketAddress.createUnresolved("localhost",
-                LwM2m.DEFAULT_COAP_PORT);
         Registration.Builder builder;
         try {
             builder = new Registration.Builder(registrationId, registrationId + "_ep",
-                    Identity.unsecure(InetAddress.getLocalHost(), 10000), registrationAddress);
+                    Identity.unsecure(InetAddress.getLocalHost(), 10000));
             return builder.build();
         } catch (UnknownHostException e) {
             throw new RuntimeException(e);
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 d7ea1902..95c84e6c 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,6 +18,10 @@ package org.eclipse.leshan.server.bootstrap;
 import static org.junit.Assert.*;
 
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
@@ -26,7 +30,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.request.exception.RequestCanceledException;
 import org.eclipse.leshan.core.response.BootstrapDeleteResponse;
 import org.eclipse.leshan.core.response.BootstrapFinishResponse;
 import org.eclipse.leshan.core.response.BootstrapResponse;
@@ -95,7 +99,7 @@ public class BootstrapHandlerTest {
         // Ensure bootstrap failed
         assertFalse(bsSessionManager.endWasCalled());
         assertTrue(bsSessionManager.failedWasCalled());
-        assertEquals(BootstrapFailureCause.FINISH_FAILED, bsSessionManager.failureCause);
+        assertEquals(BootstrapFailureCause.FINISH_FAILED, bsSessionManager.lastFailureCause);
     }
 
     @Test
@@ -104,75 +108,37 @@ public class BootstrapHandlerTest {
         // 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);
+                DefaultBootstrapHandler.DEFAULT_TIMEOUT);
 
         // 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
+        BootstrapSession firstSession = bsSessionManager.lastSession;
         assertTrue(first_response.getResponse().isSuccess());
+        assertNotNull(firstSession);
         assertFalse(bsSessionManager.endWasCalled());
         assertFalse(bsSessionManager.failedWasCalled());
 
-        // Second bootstrap for same endpoint : must be refused
+        // Second bootstrap : for the same endpoint it must be accepted and previous one should be cancelled
         bsSessionManager.reset();
+        requestSender.setMode(Mode.ALWAYS_SUCCESS);
         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());
-
-        // 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());
+        // ensure last session is accepted
+        assertTrue(second_response.getResponse().isSuccess());
         assertTrue(bsSessionManager.endWasCalled());
         assertFalse(bsSessionManager.failedWasCalled());
+        // and previous one cancelled
+        assertFalse(bsSessionManager.endWasCalled(firstSession));
+        assertTrue(bsSessionManager.failedWasCalled(firstSession, BootstrapFailureCause.CANCELLED));
     }
 
     static class MockRequestSender implements LwM2mBootstrapRequestSender {
@@ -182,14 +148,16 @@ public class BootstrapHandlerTest {
         };
 
         private Mode mode;
+        private ErrorCallback errorCallback;
 
         public MockRequestSender(Mode mode) {
             this.mode = mode;
         }
 
         @Override
-        public <T extends LwM2mResponse> T send(String clientEndpoint, Identity destination, DownlinkRequest<T> request,
-                long timeout) throws InterruptedException {
+        public <T extends LwM2mResponse> T send(BootstrapSession session, DownlinkRequest<T> request, long timeout)
+                throws InterruptedException {
+            // Not Implemented
             return null;
         }
 
@@ -199,11 +167,11 @@ public class BootstrapHandlerTest {
 
         @SuppressWarnings("unchecked")
         @Override
-        public <T extends LwM2mResponse> void send(String clientEndpoint, Identity destination,
-                DownlinkRequest<T> request, long timeout, ResponseCallback<T> responseCallback,
-                ErrorCallback errorCallback) {
+        public <T extends LwM2mResponse> void send(BootstrapSession session, DownlinkRequest<T> request, long timeout,
+                ResponseCallback<T> responseCallback, ErrorCallback errorCallback) {
             // no response, no callback call
             if (mode == Mode.NO_RESPONSE) {
+                this.errorCallback = errorCallback;
                 return;
             }
 
@@ -231,13 +199,23 @@ public class BootstrapHandlerTest {
                 }
             }
         }
+
+        @Override
+        public void cancelOngoingRequests(BootstrapSession destination) {
+            // we cancel just the last ongoing request.
+            if (errorCallback != null) {
+                errorCallback.onError(new RequestCanceledException("cancelled"));
+            }
+        }
     }
 
     private static class MockBootstrapSessionManager implements BootstrapSessionManager {
 
         private boolean authorized;
-        private BootstrapSession endBsSession = null;
-        private BootstrapFailureCause failureCause = null;
+        private BootstrapSession lastSession;
+        private BootstrapFailureCause lastFailureCause;
+        private List<BootstrapSession> endedSession = new ArrayList<BootstrapSession>();
+        private Map<BootstrapSession, BootstrapFailureCause> failureCauses = new HashMap<>();
 
         public MockBootstrapSessionManager(boolean authorized) {
             this.authorized = authorized;
@@ -245,35 +223,45 @@ public class BootstrapHandlerTest {
 
         @Override
         public BootstrapSession begin(String endpoint, Identity clientIdentity) {
-            return new DefaultBootstrapSession(endpoint, clientIdentity, authorized);
+            lastSession = new DefaultBootstrapSession(endpoint, clientIdentity, authorized);
+            return lastSession;
         }
 
-        @Override
-        public void end(BootstrapSession bsSession) {
-            endBsSession = bsSession;
+        public boolean endWasCalled() {
+            return endedSession.contains(lastSession);
         }
 
-        public boolean endWasCalled() {
-            return this.endBsSession != null;
+        public boolean endWasCalled(BootstrapSession session) {
+            return endedSession.contains(session);
         }
 
         public boolean failedWasCalled() {
-            return this.failureCause != null;
+            return failureCauses.get(lastSession) != null && this.failureCauses.get(lastSession) == lastFailureCause;
+        }
+
+        public boolean failedWasCalled(BootstrapSession session, BootstrapFailureCause cause) {
+            return failureCauses.get(session) == cause;
+        }
+
+        @Override
+        public void end(BootstrapSession bsSession) {
+            endedSession.add(bsSession);
         }
 
         @Override
         public void failed(BootstrapSession bsSession, BootstrapFailureCause cause) {
-            failureCause = cause;
+            lastFailureCause = cause;
+            failureCauses.put(bsSession, cause);
         }
 
         @Override
-        public void onResponseSuccess(BootstrapSession bsSession, LwM2mRequest<? extends LwM2mResponse> request) {
+        public void onResponseSuccess(BootstrapSession bsSession, DownlinkRequest<? extends LwM2mResponse> request) {
 
         }
 
         @Override
         public BootstrapPolicy onResponseError(BootstrapSession bsSession,
-                LwM2mRequest<? extends LwM2mResponse> request, LwM2mResponse response) {
+                DownlinkRequest<? extends LwM2mResponse> request, LwM2mResponse response) {
             if (request instanceof BootstrapFinishRequest) {
                 return BootstrapPolicy.STOP;
             }
@@ -282,13 +270,13 @@ public class BootstrapHandlerTest {
 
         @Override
         public BootstrapPolicy onRequestFailure(BootstrapSession bsSession,
-                LwM2mRequest<? extends LwM2mResponse> request, Throwable cause) {
+                DownlinkRequest<? extends LwM2mResponse> request, Throwable cause) {
             return BootstrapPolicy.STOP;
         }
 
         public void reset() {
-            endBsSession = null;
-            failureCause = null;
+            lastSession = null;
+            lastFailureCause = null;
         }
     }
 }
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java
index e8530d4b..08ce00b7 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/queue/PresenceServiceTest.java
@@ -18,7 +18,6 @@ package org.eclipse.leshan.server.queue;
 import static org.junit.Assert.*;
 
 import java.net.Inet4Address;
-import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 
 import org.eclipse.leshan.core.request.BindingMode;
@@ -62,9 +61,8 @@ public class PresenceServiceTest {
     }
 
     private Registration givenASimpleClient() throws UnknownHostException {
-        InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 5683);
         Registration.Builder builder = new Registration.Builder("ID", "urn:client",
-                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354), address);
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354));
 
         Registration reg = builder.build();
         presenceService.setAwake(reg);
@@ -72,10 +70,9 @@ public class PresenceServiceTest {
     }
 
     private Registration givenASimpleClientWithQueueMode() throws UnknownHostException {
-        InetSocketAddress address = InetSocketAddress.createUnresolved("localhost", 5683);
 
         Registration.Builder builder = new Registration.Builder("ID", "urn:client",
-                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354), address);
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354));
 
         Registration reg = builder.bindingMode(BindingMode.UQ).build();
         presenceService.setAwake(reg);
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationSortObjectLinksTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationSortObjectLinksTest.java
index 12321b2b..eb93c92c 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationSortObjectLinksTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationSortObjectLinksTest.java
@@ -17,7 +17,6 @@
 package org.eclipse.leshan.server.registration;
 
 import java.net.Inet4Address;
-import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 
 import org.eclipse.leshan.Link;
@@ -35,7 +34,7 @@ public class RegistrationSortObjectLinksTest {
         objs[2] = null;
 
         Registration.Builder builder = new Registration.Builder("registrationId", "endpoint",
-                Identity.unsecure(Inet4Address.getLocalHost(), 1), new InetSocketAddress(212)).objectLinks(objs);
+                Identity.unsecure(Inet4Address.getLocalHost(), 1)).objectLinks(objs);
 
         Registration r = builder.build();
 
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java
index cd1a5d6f..ac289c8b 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java
@@ -88,8 +88,7 @@ public class RegistrationTest {
 
     private Registration given_a_registration_with_object_link_like(String objectLinks) {
         Builder builder = new Registration.Builder("id", "endpoin",
-                Identity.unsecure(InetSocketAddress.createUnresolved("localhost", 0)),
-                InetSocketAddress.createUnresolved("localhost", 0));
+                Identity.unsecure(InetSocketAddress.createUnresolved("localhost", 0)));
 
         builder.objectLinks(Link.parse(objectLinks.getBytes()));
         return builder.build();
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationUpdateTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationUpdateTest.java
index 171e8a9a..3cf9e3f8 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationUpdateTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationUpdateTest.java
@@ -16,7 +16,6 @@
 package org.eclipse.leshan.server.registration;
 
 import java.net.Inet4Address;
-import java.net.InetSocketAddress;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -34,7 +33,7 @@ public class RegistrationUpdateTest {
     @Test
     public void testAdditionalAttributesUpdate() throws Exception {
         Registration.Builder builder = new Registration.Builder("registrationId", "endpoint",
-                Identity.unsecure(Inet4Address.getLocalHost(), 1), new InetSocketAddress(212));
+                Identity.unsecure(Inet4Address.getLocalHost(), 1));
 
         Map<String, String> additionalAttributes = new HashMap<String, String>();
         additionalAttributes.put("x", "1");
diff --git a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java
index b80b0a3d..5cba1dd5 100644
--- a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java
+++ b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java
@@ -18,14 +18,12 @@ package org.eclipse.leshan.server.redis.serialization;
 import static org.junit.Assert.assertEquals;
 
 import java.net.Inet4Address;
-import java.net.InetSocketAddress;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.eclipse.leshan.Link;
 import org.eclipse.leshan.core.request.Identity;
-import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
 import org.eclipse.leshan.server.registration.Registration;
 import org.junit.Test;
 
@@ -42,7 +40,7 @@ public class RegistrationSerDesTest {
         objs[1] = new Link("/0/2");
 
         Registration.Builder builder = new Registration.Builder("registrationId", "endpoint",
-                Identity.unsecure(Inet4Address.getLoopbackAddress(), 1), new InetSocketAddress(212)).objectLinks(objs);
+                Identity.unsecure(Inet4Address.getLoopbackAddress(), 1)).objectLinks(objs);
 
         builder.registrationDate(new Date(100L));
         builder.lastUpdate(new Date(101L));