Repository: eclipse/leshan
Pull request #816 (2020-03-05 17:32:16)

Enhance RegistrationEngine
All this changes are mainly driven by needs for : https://github.com/sbernard31/benchmark-clients


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
index dac298a8..61eae6af 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/CaliforniumEndpointsManager.java
@@ -190,13 +190,41 @@ public class CaliforniumEndpointsManager implements EndpointsManager {
     }
 
     @Override
-    public synchronized void forceReconnection(Server server) {
+    public long getMaxCommunicationPeriodFor(Server server, long lifetimeInMs) {
+        // See https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/283 to better understand.
+        // TODO For DTLS, worst Handshake scenario should be taking into account too.
+
+        int floor = 30000; // value from which we stop to adjust communication period using COAP EXCHANGE LIFETIME.
+
+        // To be sure registration doesn't expired, update request should be send considering all CoAP retransmissions
+        // and registration lifetime.
+        // See https://tools.ietf.org/html/rfc7252#section-4.8.2
+        long exchange_lifetime = coapConfig.getLong(NetworkConfig.Keys.EXCHANGE_LIFETIME, 247);
+        if (lifetimeInMs - exchange_lifetime >= floor) {
+            return lifetimeInMs - exchange_lifetime;
+        } else {
+            LOG.warn("Too small lifetime : we advice to not use a lifetime < (COAP EXCHANGE LIFETIME + 30s)");
+            // lifetime value is too short, so we do a compromise and we don't remove COAP EXCHANGE LIFETIME completely
+            // We distribute the remaining lifetime range [0, exchange_lifetime + floor] on the remaining range
+            // [1,floor]s.
+            return lifetimeInMs * (floor - 1000) / (exchange_lifetime + floor) + 1000;
+        }
+    }
+
+    @Override
+    public synchronized void forceReconnection(Server server, boolean resume) {
         // TODO support multi server
         Connector connector = currentEndpoint.getConnector();
         if (connector instanceof DTLSConnector) {
-            ((DTLSConnector) connector).forceResumeAllSessions();
+            if (resume) {
+                LOG.info("Clear DTLS session for resumption for server {}", server.getUri());
+                ((DTLSConnector) connector).forceResumeAllSessions();
+            } else {
+                LOG.info("Clear DTLS session for server {}", server.getUri());
+                ((DTLSConnector) connector).clearConnectionState();
+            }
         }
-        LOG.info("Clear DTLS session for server {}", server.getUri());
+
     }
 
     public synchronized Endpoint getEndpoint(Identity server) {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/EndpointsManager.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/EndpointsManager.java
index 603179bd..c2c2590d 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/EndpointsManager.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/EndpointsManager.java
@@ -26,7 +26,9 @@ public interface EndpointsManager {
 
     Collection<Server> createEndpoints(Collection<? extends ServerInfo> serverInfo);
 
-    void forceReconnection(Server server);
+    long getMaxCommunicationPeriodFor(Server server, long lifetimeInSeconds);
+
+    void forceReconnection(Server server, boolean resume);
 
     void start();
 
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
index bc2a6c3a..0bafd17f 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
@@ -70,15 +70,20 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
 
     private static final long NOW = 0;
 
-    // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
-    // send a Confirmable message to the time when an acknowledgement is no longer expected.
+    // Timeout for bootstrap/register/update request
     private final long requestTimeoutInMs;
     // de-registration is only used on stop/destroy for now.
     private final long deregistrationTimeoutInMs;
-    // bootstrap timeout
+    // Bootstrap session timeout
     private final int bootstrapSessionTimeoutInSec;
-    // time between bootstrap retry should be configurable and incremental
+    // Time between bootstrap retry should incremental
     private final int retryWaitingTimeInMs;
+    // Time between 2 update requests (used only if it is smaller than the lifetime)
+    private Integer communicationPeriodInMs;
+    // True if client should re-initiate a connection (DTLS) on registration update
+    private boolean reconnectOnUpdate;
+    // True if client should try to resume connection if possible.
+    private boolean resumeOnConnect;
 
     private static enum Status {
         SUCCESS, FAILURE, TIMEOUT
@@ -108,7 +113,8 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
     public DefaultRegistrationEngine(String endpoint, LwM2mObjectTree objectTree, EndpointsManager endpointsManager,
             LwM2mRequestSender requestSender, BootstrapHandler bootstrapState, LwM2mClientObserver observer,
             Map<String, String> additionalAttributes, ScheduledExecutorService executor, long requestTimeoutInMs,
-            long deregistrationTimeoutInMs, int bootstrapSessionTimeoutInSec, int retryWaitingTimeInMs) {
+            long deregistrationTimeoutInMs, int bootstrapSessionTimeoutInSec, int retryWaitingTimeInMs,
+            Integer communicationPeriodInMs, boolean reconnectOnUpdate, boolean resumeOnConnect) {
         this.endpoint = endpoint;
         this.objectEnablers = objectTree.getObjectEnablers();
         this.bootstrapHandler = bootstrapState;
@@ -120,6 +126,9 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         this.deregistrationTimeoutInMs = deregistrationTimeoutInMs;
         this.bootstrapSessionTimeoutInSec = bootstrapSessionTimeoutInSec;
         this.retryWaitingTimeInMs = retryWaitingTimeInMs;
+        this.communicationPeriodInMs = communicationPeriodInMs;
+        this.reconnectOnUpdate = reconnectOnUpdate;
+        this.resumeOnConnect = resumeOnConnect;
 
         if (executor == null) {
             schedExecutor = createScheduledExecutor();
@@ -184,13 +193,18 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
             Server bootstrapServer = endpointsManager.createEndpoint(bootstrapServerInfo);
 
             // Send bootstrap request
+            BootstrapRequest request = null;
             try {
+                request = new BootstrapRequest(endpoint);
+                if (observer != null) {
+                    observer.onBootstrapStarted(bootstrapServer, request);
+                }
                 BootstrapResponse response = sender.send(bootstrapServerInfo.getAddress(),
-                        bootstrapServerInfo.isSecure(), new BootstrapRequest(endpoint), requestTimeoutInMs);
+                        bootstrapServerInfo.isSecure(), request, requestTimeoutInMs);
                 if (response == null) {
-                    LOG.error("Unable to start bootstrap session: Timeout.");
+                    LOG.info("Unable to start bootstrap session: Timeout.");
                     if (observer != null) {
-                        observer.onBootstrapTimeout(bootstrapServer);
+                        observer.onBootstrapTimeout(bootstrapServer, request);
                     }
                     return null;
                 } else if (response.isSuccess()) {
@@ -198,9 +212,9 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
                     // Wait until it is finished (or too late)
                     boolean timeout = !bootstrapHandler.waitBoostrapFinished(bootstrapSessionTimeoutInSec);
                     if (timeout) {
-                        LOG.error("Bootstrap sequence aborted: Timeout.");
+                        LOG.info("Bootstrap sequence aborted: Timeout.");
                         if (observer != null) {
-                            observer.onBootstrapTimeout(bootstrapServer);
+                            observer.onBootstrapTimeout(bootstrapServer, request);
                         }
                         return null;
                     } else {
@@ -211,19 +225,23 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
                             dmServers = endpointsManager.createEndpoints(serverInfos.deviceManagements.values());
                         }
                         if (observer != null) {
-                            observer.onBootstrapSuccess(bootstrapServer);
+                            observer.onBootstrapSuccess(bootstrapServer, request);
                         }
                         return dmServers;
                     }
                 } else {
-                    LOG.error("Bootstrap failed: {} {}.", response.getCode(), response.getErrorMessage());
+                    LOG.info("Bootstrap failed: {} {}.", response.getCode(), response.getErrorMessage());
                     if (observer != null) {
-                        observer.onBootstrapFailure(bootstrapServer, response.getCode(), response.getErrorMessage());
+                        observer.onBootstrapFailure(bootstrapServer, request, response.getCode(),
+                                response.getErrorMessage(), null);
                     }
                     return null;
                 }
-            } catch (SendFailedException e) {
+            } catch (RuntimeException e) {
                 logExceptionOnSendRequest("Unable to send Bootstrap request", e);
+                if (observer != null) {
+                    observer.onBootstrapFailure(bootstrapServer, request, null, null, e);
+                }
                 return null;
             } finally {
                 bootstrapHandler.closeSession();
@@ -239,7 +257,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         if (registerStatus == Status.TIMEOUT) {
             // if register timeout maybe server lost the session,
             // so we reconnect (new handshake) and retry
-            endpointsManager.forceReconnection(server);
+            endpointsManager.forceReconnection(server, resumeOnConnect);
             registerStatus = register(server);
         }
         return registerStatus == Status.SUCCESS;
@@ -249,22 +267,26 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         DmServerInfo dmInfo = ServersInfoExtractor.getDMServerInfo(objectEnablers, server.getId());
 
         if (dmInfo == null) {
-            LOG.error("Trying to register device but there is no LWM2M server config.");
+            LOG.info("Trying to register device but there is no LWM2M server config.");
             return Status.FAILURE;
         }
 
         // Send register request
         LOG.info("Trying to register to {} ...", server.getUri());
+        RegisterRequest request = null;
         try {
-            RegisterRequest regRequest = new RegisterRequest(endpoint, dmInfo.lifetime, LwM2m.VERSION, dmInfo.binding,
-                    null, LinkFormatHelper.getClientDescription(objectEnablers.values(), null), additionalAttributes);
-            RegisterResponse response = sender.send(dmInfo.getAddress(), dmInfo.isSecure(), regRequest,
+            request = new RegisterRequest(endpoint, dmInfo.lifetime, LwM2m.VERSION, dmInfo.binding, null,
+                    LinkFormatHelper.getClientDescription(objectEnablers.values(), null), additionalAttributes);
+            if (observer != null) {
+                observer.onRegistrationStarted(server, request);
+            }
+            RegisterResponse response = sender.send(dmInfo.getAddress(), dmInfo.isSecure(), request,
                     requestTimeoutInMs);
 
             if (response == null) {
-                LOG.error("Registration failed: Timeout.");
+                LOG.info("Registration failed: Timeout.");
                 if (observer != null) {
-                    observer.onRegistrationTimeout(server);
+                    observer.onRegistrationTimeout(server, request);
                 }
                 return Status.TIMEOUT;
             } else if (response.isSuccess()) {
@@ -274,22 +296,26 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
                 LOG.info("Registered with location '{}'.", registrationID);
 
                 // Update every lifetime period
-                long delay = calculateNextUpdate(dmInfo.lifetime);
+                long delay = calculateNextUpdate(server, dmInfo.lifetime);
                 scheduleUpdate(server, registrationID, new RegistrationUpdate(), delay);
 
                 if (observer != null) {
-                    observer.onRegistrationSuccess(server, registrationID);
+                    observer.onRegistrationSuccess(server, request, registrationID);
                 }
                 return Status.SUCCESS;
             } else {
-                LOG.error("Registration failed: {} {}.", response.getCode(), response.getErrorMessage());
+                LOG.info("Registration failed: {} {}.", response.getCode(), response.getErrorMessage());
                 if (observer != null) {
-                    observer.onRegistrationFailure(server, response.getCode(), response.getErrorMessage());
+                    observer.onRegistrationFailure(server, request, response.getCode(), response.getErrorMessage(),
+                            null);
                 }
                 return Status.FAILURE;
             }
-        } catch (SendFailedException e) {
+        } catch (RuntimeException e) {
             logExceptionOnSendRequest("Unable to send register request", e);
+            if (observer != null) {
+                observer.onRegistrationFailure(server, request, null, null, e);
+            }
             return Status.FAILURE;
         }
     }
@@ -300,20 +326,25 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
 
         DmServerInfo dmInfo = ServersInfoExtractor.getDMServerInfo(objectEnablers, server.getId());
         if (dmInfo == null) {
-            LOG.error("Trying to deregister device but there is no LWM2M server config.");
+            LOG.info("Trying to deregister device but there is no LWM2M server config.");
             return false;
         }
 
         // Send deregister request
         LOG.info("Trying to deregister to {} ...", server.getUri());
+        DeregisterRequest request = null;
         try {
+            request = new DeregisterRequest(registrationID);
+            if (observer != null) {
+                observer.onDeregistrationStarted(server, request);
+            }
             DeregisterResponse response = sender.send(server.getIdentity().getPeerAddress(),
-                    server.getIdentity().isSecure(), new DeregisterRequest(registrationID), deregistrationTimeoutInMs);
+                    server.getIdentity().isSecure(), request, deregistrationTimeoutInMs);
             if (response == null) {
                 registrationID = null;
-                LOG.error("Deregistration failed: Timeout.");
+                LOG.info("Deregistration failed: Timeout.");
                 if (observer != null) {
-                    observer.onDeregistrationTimeout(server);
+                    observer.onDeregistrationTimeout(server, request);
                 }
                 return false;
             } else if (response.isSuccess() || response.getCode() == ResponseCode.NOT_FOUND) {
@@ -323,21 +354,26 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
                 LOG.info("De-register response {} {}.", response.getCode(), response.getErrorMessage());
                 if (observer != null) {
                     if (response.isSuccess()) {
-                        observer.onDeregistrationSuccess(server, registrationID);
+                        observer.onDeregistrationSuccess(server, request);
                     } else {
-                        observer.onDeregistrationFailure(server, response.getCode(), response.getErrorMessage());
+                        observer.onDeregistrationFailure(server, request, response.getCode(),
+                                response.getErrorMessage(), null);
                     }
                 }
                 return true;
             } else {
-                LOG.error("Deregistration failed: {} {}.", response.getCode(), response.getErrorMessage());
+                LOG.info("Deregistration failed: {} {}.", response.getCode(), response.getErrorMessage());
                 if (observer != null) {
-                    observer.onDeregistrationFailure(server, response.getCode(), response.getErrorMessage());
+                    observer.onDeregistrationFailure(server, request, response.getCode(), response.getErrorMessage(),
+                            null);
                 }
                 return false;
             }
-        } catch (SendFailedException e) {
+        } catch (RuntimeException e) {
             logExceptionOnSendRequest("Unable to send deregister request", e);
+            if (observer != null) {
+                observer.onDeregistrationFailure(server, request, null, null, e);
+            }
             return false;
         }
     }
@@ -349,7 +385,7 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         if (updateStatus == Status.TIMEOUT) {
             // if register timeout maybe server lost the session,
             // so we reconnect (new handshake) and retry
-            endpointsManager.forceReconnection(server);
+            endpointsManager.forceReconnection(server, resumeOnConnect);
             updateStatus = update(server, registrationId, registrationUpdate);
         }
         return updateStatus == Status.SUCCESS;
@@ -359,52 +395,64 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
             throws InterruptedException {
         DmServerInfo dmInfo = ServersInfoExtractor.getDMServerInfo(objectEnablers, server.getId());
         if (dmInfo == null) {
-            LOG.error("Trying to update registration but there is no LWM2M server config.");
+            LOG.info("Trying to update registration but there is no LWM2M server config.");
             return Status.FAILURE;
         }
 
         // Send update
-        LOG.info("Trying to update registration to {} ...", server.getUri());
+        LOG.info("Trying to update registration to {} (response timeout {}ms)...", server.getUri(), requestTimeoutInMs);
+        UpdateRequest request = null;
         try {
-            UpdateResponse response = sender.send(dmInfo.getAddress(), dmInfo.isSecure(),
-                    new UpdateRequest(registrationID, registrationUpdate.getLifeTimeInSec(),
-                            registrationUpdate.getSmsNumber(), registrationUpdate.getBindingMode(),
-                            registrationUpdate.getObjectLinks(), registrationUpdate.getAdditionalAttributes()),
-                    requestTimeoutInMs);
+            request = new UpdateRequest(registrationID, registrationUpdate.getLifeTimeInSec(),
+                    registrationUpdate.getSmsNumber(), registrationUpdate.getBindingMode(),
+                    registrationUpdate.getObjectLinks(), registrationUpdate.getAdditionalAttributes());
+            if (observer != null) {
+                observer.onUpdateStarted(server, request);
+            }
+            if (reconnectOnUpdate) {
+                endpointsManager.forceReconnection(server, resumeOnConnect);
+            }
+            UpdateResponse response = sender.send(dmInfo.getAddress(), dmInfo.isSecure(), request, requestTimeoutInMs);
             if (response == null) {
                 registrationID = null;
-                LOG.error("Registration update failed: Timeout.");
+                LOG.info("Registration update failed: Timeout.");
                 if (observer != null) {
-                    observer.onUpdateTimeout(server);
+                    observer.onUpdateTimeout(server, request);
                 }
                 return Status.TIMEOUT;
             } else if (response.getCode() == ResponseCode.CHANGED) {
                 // Update successful, so we reschedule new update
                 LOG.info("Registration update succeed.");
-                long delay = calculateNextUpdate(dmInfo.lifetime);
+                long delay = calculateNextUpdate(server, dmInfo.lifetime);
                 scheduleUpdate(server, registrationID, new RegistrationUpdate(), delay);
                 if (observer != null) {
-                    observer.onUpdateSuccess(server, registrationID);
+                    observer.onUpdateSuccess(server, request);
                 }
                 return Status.SUCCESS;
             } else {
-                LOG.error("Registration update failed: {} {}.", response.getCode(), response.getErrorMessage());
+                LOG.info("Registration update failed: {} {}.", response.getCode(), response.getErrorMessage());
                 if (observer != null) {
-                    observer.onUpdateFailure(server, response.getCode(), response.getErrorMessage());
+                    observer.onUpdateFailure(server, request, response.getCode(), response.getErrorMessage(), null);
                 }
                 registeredServers.remove(registrationID);
                 return Status.FAILURE;
             }
-        } catch (SendFailedException e) {
+        } catch (RuntimeException e) {
             logExceptionOnSendRequest("Unable to send update request", e);
+            if (observer != null) {
+                observer.onUpdateFailure(server, request, null, null, e);
+            }
             return Status.FAILURE;
         }
     }
 
-    private long calculateNextUpdate(long lifetimeInSeconds) {
-        // lifetime - 10%
-        // life time is in seconds and we return the delay in milliseconds
-        return lifetimeInSeconds * 900l;
+    private long calculateNextUpdate(Server server, long lifetimeInSeconds) {
+        long maxComminucationPeriod = endpointsManager.getMaxCommunicationPeriodFor(server, lifetimeInSeconds * 1000);
+        if (communicationPeriodInMs != null) {
+            return Math.min(communicationPeriodInMs, maxComminucationPeriod);
+        } else {
+            return maxComminucationPeriod;
+        }
     }
 
     private synchronized boolean scheduleClientInitiatedBootstrap(long timeInMs) {
@@ -659,11 +707,11 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         }
         if (e instanceof SendFailedException) {
             if (e.getCause() != null && e.getMessage() != null) {
-                LOG.warn("{} : {}", message, e.getCause().getMessage());
+                LOG.info("{} : {}", message, e.getCause().getMessage());
                 return;
             }
         }
-        LOG.warn("{} : {}", message, e.getMessage());
+        LOG.info("{} : {}", message, e.getMessage());
     }
 
     /**
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
index e3e4f465..3e5b3044 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngineFactory.java
@@ -30,6 +30,9 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
     private long deregistrationTimeoutInMs = 1000; // 1s in ms
     private int bootstrapSessionTimeoutInSec = 93;
     private int retryWaitingTimeInMs = 10 * 60 * 1000; // 10min in ms
+    private Integer communicationPeriodInMs = null;
+    private boolean reconnectOnUpdate = false;
+    private boolean resumeOnConnect = true;
 
     public DefaultRegistrationEngineFactory() {
     }
@@ -41,7 +44,20 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
             ScheduledExecutorService sharedExecutor) {
         return new DefaultRegistrationEngine(endpoint, objectTree, endpointsManager, requestSender, bootstrapState,
                 observer, additionalAttributes, sharedExecutor, requestTimeoutInMs, deregistrationTimeoutInMs,
-                bootstrapSessionTimeoutInSec, retryWaitingTimeInMs);
+                bootstrapSessionTimeoutInSec, retryWaitingTimeInMs, communicationPeriodInMs, reconnectOnUpdate,
+                resumeOnConnect);
+    }
+
+    /**
+     * Set the period between 2 communications (update request).
+     * <p>
+     * Client will communicate periodically to refresh its lifetime but if you want to communication periodically more
+     * often you can set a smaller communication period.
+     * 
+     * @param communicationPeriodInMs the communication period in ms
+     */
+    public void setCommunicationPeriod(Integer communicationPeriodInMs) {
+        this.communicationPeriodInMs = communicationPeriodInMs;
     }
 
     /**
@@ -102,4 +118,31 @@ public class DefaultRegistrationEngineFactory implements RegistrationEngineFacto
         this.retryWaitingTimeInMs = retryWaitingTimeInMs;
         return this;
     }
+
+    /**
+     * Configure if client reconnects before update. For DTLS "reconnect" means "initiate a new handshake".
+     * <p>
+     * Default is false.
+     * 
+     * @param reconnectOnUpdate True is client should reconnect on update
+     * @return this for fluent API
+     */
+    public DefaultRegistrationEngineFactory setReconnectOnUpdate(boolean reconnectOnUpdate) {
+        this.reconnectOnUpdate = reconnectOnUpdate;
+        return this;
+    }
+
+    /**
+     * Configure if client tries to resume a session. For DTLS this means that when a new handshake is initiated we will
+     * try to do an abbreviated one (instead a full one) if possible.
+     * <p>
+     * Default value is true
+     * 
+     * @param resumeOnConnect True if client should try to resume on connect
+     * @return this for fluent API
+     */
+    public DefaultRegistrationEngineFactory setResumeOnConnect(boolean resumeOnConnect) {
+        this.resumeOnConnect = resumeOnConnect;
+        return this;
+    }
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserver.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserver.java
index e0a1f1f1..580eb218 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserver.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserver.java
@@ -17,33 +17,56 @@ package org.eclipse.leshan.client.observer;
 
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.client.servers.Server;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.DeregisterRequest;
+import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.UpdateRequest;
 
 /**
  * Allow to observer the registration life cycle of a LwM2m client.
  */
 public interface LwM2mClientObserver {
+    // ============== Bootstrap =================
 
-    void onBootstrapSuccess(Server bsserver);
+    void onBootstrapStarted(Server bsserver, BootstrapRequest request);
 
-    void onBootstrapFailure(Server bsserver, ResponseCode responseCode, String errorMessage);
+    void onBootstrapSuccess(Server bsserver, BootstrapRequest request);
 
-    void onBootstrapTimeout(Server bsserver);
+    void onBootstrapFailure(Server bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause);
 
-    void onRegistrationSuccess(Server server, String registrationID);
+    void onBootstrapTimeout(Server bsserver, BootstrapRequest request);
 
-    void onRegistrationFailure(Server server, ResponseCode responseCode, String errorMessage);
+    // ============== Registration =================
 
-    void onRegistrationTimeout(Server server);
+    void onRegistrationStarted(Server server, RegisterRequest request);
 
-    void onUpdateSuccess(Server server, String registrationID);
+    void onRegistrationSuccess(Server server, RegisterRequest request, String registrationID);
 
-    void onUpdateFailure(Server server, ResponseCode responseCode, String errorMessage);
+    void onRegistrationFailure(Server server, RegisterRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause);
 
-    void onUpdateTimeout(Server server);
+    void onRegistrationTimeout(Server server, RegisterRequest request);
 
-    void onDeregistrationSuccess(Server server, String registrationID);
+    // ============== Registration Update =================
 
-    void onDeregistrationFailure(Server server, ResponseCode responseCode, String errorMessage);
+    void onUpdateStarted(Server server, UpdateRequest request);
 
-    void onDeregistrationTimeout(Server server);
+    void onUpdateSuccess(Server server, UpdateRequest request);
+
+    void onUpdateFailure(Server server, UpdateRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause);
+
+    void onUpdateTimeout(Server server, UpdateRequest request);
+
+    // ============== Deregistration Update =================
+
+    void onDeregistrationStarted(Server server, DeregisterRequest request);
+
+    void onDeregistrationSuccess(Server server, DeregisterRequest request);
+
+    void onDeregistrationFailure(Server server, DeregisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause);
+
+    void onDeregistrationTimeout(Server server, DeregisterRequest request);
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverAdapter.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverAdapter.java
index 1af404bc..43810725 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverAdapter.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverAdapter.java
@@ -17,6 +17,10 @@ package org.eclipse.leshan.client.observer;
 
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.client.servers.Server;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.DeregisterRequest;
+import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.UpdateRequest;
 
 /**
  * An abstract adapter class for observing registration life cycle. The methods in this class are empty. This class
@@ -25,50 +29,70 @@ import org.eclipse.leshan.client.servers.Server;
 public class LwM2mClientObserverAdapter implements LwM2mClientObserver {
 
     @Override
-    public void onBootstrapSuccess(Server bsserver) {
+    public void onBootstrapStarted(Server bsserver, BootstrapRequest request) {
     }
 
     @Override
-    public void onBootstrapFailure(Server bsserver, ResponseCode responseCode, String errorMessage) {
+    public void onBootstrapSuccess(Server bsserver, BootstrapRequest request) {
     }
 
     @Override
-    public void onBootstrapTimeout(Server bsserver) {
+    public void onBootstrapFailure(Server bsserver, BootstrapRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
     }
 
     @Override
-    public void onRegistrationSuccess(Server server, String registrationID) {
+    public void onBootstrapTimeout(Server bsserver, BootstrapRequest request) {
     }
 
     @Override
-    public void onRegistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onRegistrationStarted(Server server, RegisterRequest request) {
     }
 
     @Override
-    public void onRegistrationTimeout(Server server) {
+    public void onRegistrationSuccess(Server server, RegisterRequest request, String registrationID) {
     }
 
     @Override
-    public void onUpdateSuccess(Server server, String registrationID) {
+    public void onRegistrationFailure(Server server, RegisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
     }
 
     @Override
-    public void onUpdateFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onRegistrationTimeout(Server server, RegisterRequest request) {
     }
 
     @Override
-    public void onUpdateTimeout(Server server) {
+    public void onUpdateStarted(Server server, UpdateRequest request) {
     }
 
     @Override
-    public void onDeregistrationSuccess(Server server, String registrationID) {
+    public void onUpdateSuccess(Server server, UpdateRequest request) {
     }
 
     @Override
-    public void onDeregistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onUpdateFailure(Server server, UpdateRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause) {
     }
 
     @Override
-    public void onDeregistrationTimeout(Server server) {
+    public void onUpdateTimeout(Server server, UpdateRequest request) {
+    }
+
+    @Override
+    public void onDeregistrationStarted(Server server, DeregisterRequest request) {
+    }
+
+    @Override
+    public void onDeregistrationSuccess(Server server, DeregisterRequest request) {
+    }
+
+    @Override
+    public void onDeregistrationFailure(Server server, DeregisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
+    }
+
+    @Override
+    public void onDeregistrationTimeout(Server server, DeregisterRequest request) {
     }
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverDispatcher.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverDispatcher.java
index 635d3bb7..b9f97e3f 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverDispatcher.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/observer/LwM2mClientObserverDispatcher.java
@@ -15,6 +15,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.client.servers.Server;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.DeregisterRequest;
+import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.UpdateRequest;
 
 /**
  * A dispatcher for LwM2mClientObserver. It allow several observers on a LwM2mClient.
@@ -32,86 +36,118 @@ public class LwM2mClientObserverDispatcher implements LwM2mClientObserver {
     }
 
     @Override
-    public void onBootstrapSuccess(Server bsserver) {
+    public void onBootstrapStarted(Server bsserver, BootstrapRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onBootstrapSuccess(bsserver);
+            observer.onBootstrapStarted(bsserver, request);
         }
     }
 
     @Override
-    public void onBootstrapFailure(Server bsserver, ResponseCode responseCode, String errorMessage) {
+    public void onBootstrapSuccess(Server bsserver, BootstrapRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onBootstrapFailure(bsserver, responseCode, errorMessage);
+            observer.onBootstrapSuccess(bsserver, request);
         }
     }
 
     @Override
-    public void onBootstrapTimeout(Server bsserver) {
+    public void onBootstrapFailure(Server bsserver, BootstrapRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onBootstrapTimeout(bsserver);
+            observer.onBootstrapFailure(bsserver, request, responseCode, errorMessage, cause);
         }
     }
 
     @Override
-    public void onRegistrationSuccess(Server server, String registrationID) {
+    public void onBootstrapTimeout(Server bsserver, BootstrapRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onRegistrationSuccess(server, registrationID);
+            observer.onBootstrapTimeout(bsserver, request);
         }
     }
 
     @Override
-    public void onRegistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onRegistrationStarted(Server server, RegisterRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onRegistrationFailure(server, responseCode, errorMessage);
+            observer.onRegistrationStarted(server, request);
         }
     }
 
     @Override
-    public void onRegistrationTimeout(Server server) {
+    public void onRegistrationSuccess(Server server, RegisterRequest request, String registrationID) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onRegistrationTimeout(server);
+            observer.onRegistrationSuccess(server, request, registrationID);
         }
     }
 
     @Override
-    public void onUpdateSuccess(Server server, String registrationID) {
+    public void onRegistrationFailure(Server server, RegisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onUpdateSuccess(server, registrationID);
+            observer.onRegistrationFailure(server, request, responseCode, errorMessage, cause);
         }
     }
 
     @Override
-    public void onUpdateFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onRegistrationTimeout(Server server, RegisterRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onUpdateFailure(server, responseCode, errorMessage);
+            observer.onRegistrationTimeout(server, request);
         }
     }
 
     @Override
-    public void onUpdateTimeout(Server server) {
+    public void onUpdateStarted(Server server, UpdateRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onUpdateTimeout(server);
+            observer.onUpdateStarted(server, request);
         }
     }
 
     @Override
-    public void onDeregistrationSuccess(Server server, String registrationID) {
+    public void onUpdateSuccess(Server server, UpdateRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onDeregistrationSuccess(server, registrationID);
+            observer.onUpdateSuccess(server, request);
         }
     }
 
     @Override
-    public void onDeregistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onUpdateFailure(Server server, UpdateRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onDeregistrationFailure(server, responseCode, errorMessage);
+            observer.onUpdateFailure(server, request, responseCode, errorMessage, cause);
         }
     }
 
     @Override
-    public void onDeregistrationTimeout(Server server) {
+    public void onUpdateTimeout(Server server, UpdateRequest request) {
         for (LwM2mClientObserver observer : observers) {
-            observer.onDeregistrationTimeout(server);
+            observer.onUpdateTimeout(server, request);
+        }
+    }
+
+    @Override
+    public void onDeregistrationStarted(Server server, DeregisterRequest request) {
+        for (LwM2mClientObserver observer : observers) {
+            observer.onDeregistrationStarted(server, request);
+        }
+    }
+
+    @Override
+    public void onDeregistrationSuccess(Server server, DeregisterRequest request) {
+        for (LwM2mClientObserver observer : observers) {
+            observer.onDeregistrationSuccess(server, request);
+        }
+    }
+
+    @Override
+    public void onDeregistrationFailure(Server server, DeregisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception e) {
+        for (LwM2mClientObserver observer : observers) {
+            observer.onDeregistrationFailure(server, request, responseCode, errorMessage, e);
+        }
+    }
+
+    @Override
+    public void onDeregistrationTimeout(Server server, DeregisterRequest request) {
+        for (LwM2mClientObserver observer : observers) {
+            observer.onDeregistrationTimeout(server, request);
         }
     }
 
diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
index 1a0704d5..6761de25 100644
--- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
+++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
@@ -49,6 +49,7 @@ import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
 import org.eclipse.leshan.LwM2m;
 import org.eclipse.leshan.client.californium.LeshanClient;
 import org.eclipse.leshan.client.californium.LeshanClientBuilder;
+import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
 import org.eclipse.leshan.client.object.Server;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
@@ -143,11 +144,15 @@ public class LeshanClientDemo {
         options.addOption("b", false, "If present use bootstrap.");
         options.addOption("l", true, String.format(
                 "The lifetime in seconds used to register, ignored if -b is used.\n Default : %ds", DEFAULT_LIFETIME));
+        options.addOption("cp", true,
+                "The communication period in seconds which should be smaller than the lifeime, will be used even if -b is used.");
         options.addOption("lh", true, "Set the local CoAP address of the Client.\n  Default: any local address.");
         options.addOption("lp", true,
                 "Set the local CoAP port of the Client.\n  Default: A valid port value is between 0 and 65535.");
         options.addOption("u", true, String.format("Set the LWM2M or Bootstrap server URL.\nDefault: localhost:%d.",
                 LwM2m.DEFAULT_COAP_PORT));
+        options.addOption("r", false, "Force reconnect/rehandshake on update.");
+        options.addOption("f", false, "Do not try to resume session always, do a full handshake.");
         options.addOption("ocf",
                 "activate support of old/unofficial content format .\n See https://github.com/eclipse/leshan/pull/720");
         options.addOption("oc", "activate support of old/deprecated cipher suites.");
@@ -261,6 +266,12 @@ public class LeshanClientDemo {
             lifetime = DEFAULT_LIFETIME;
         }
 
+        // Get lifetime
+        Integer communicationPeriod = null;
+        if (cl.hasOption("cp")) {
+            communicationPeriod = Integer.valueOf(cl.getOptionValue("cp")) * 1000;
+        }
+
         // Get additional attributes
         Map<String, String> additionalAttributes = null;
         if (cl.hasOption("aa")) {
@@ -385,9 +396,9 @@ public class LeshanClientDemo {
         }
         try {
             createAndStartClient(endpoint, localAddress, localPort, cl.hasOption("b"), additionalAttributes, lifetime,
-                    serverURI, pskIdentity, pskKey, clientPrivateKey, clientPublicKey, serverPublicKey,
-                    clientCertificate, serverCertificate, latitude, longitude, scaleFactor, cl.hasOption("ocf"),
-                    cl.hasOption("oc"));
+                    communicationPeriod, serverURI, pskIdentity, pskKey, clientPrivateKey, clientPublicKey,
+                    serverPublicKey, clientCertificate, serverCertificate, latitude, longitude, scaleFactor,
+                    cl.hasOption("ocf"), cl.hasOption("oc"), cl.hasOption("r"), cl.hasOption("f"));
         } catch (Exception e) {
             System.err.println("Unable to create and start client ...");
             e.printStackTrace();
@@ -396,10 +407,11 @@ public class LeshanClientDemo {
     }
 
     public static void createAndStartClient(String endpoint, String localAddress, int localPort, boolean needBootstrap,
-            Map<String, String> additionalAttributes, int lifetime, String serverURI, byte[] pskIdentity, byte[] pskKey,
-            PrivateKey clientPrivateKey, PublicKey clientPublicKey, PublicKey serverPublicKey,
-            X509Certificate clientCertificate, X509Certificate serverCertificate, Float latitude, Float longitude,
-            float scaleFactor, boolean supportOldFormat, boolean supportDeprecatedCiphers)
+            Map<String, String> additionalAttributes, int lifetime, Integer communicationPeriod, String serverURI,
+            byte[] pskIdentity, byte[] pskKey, PrivateKey clientPrivateKey, PublicKey clientPublicKey,
+            PublicKey serverPublicKey, X509Certificate clientCertificate, X509Certificate serverCertificate,
+            Float latitude, Float longitude, float scaleFactor, boolean supportOldFormat,
+            boolean supportDeprecatedCiphers, boolean reconnectOnUpdate, boolean forceFullhandshake)
             throws CertificateEncodingException {
 
         locationInstance = new MyLocation(latitude, longitude, scaleFactor);
@@ -464,12 +476,19 @@ public class LeshanClientDemo {
         DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();
         dtlsConfig.setRecommendedCipherSuitesOnly(!supportDeprecatedCiphers);
 
+        // Configure Registration Engine
+        DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory();
+        engineFactory.setCommunicationPeriod(communicationPeriod);
+        engineFactory.setReconnectOnUpdate(reconnectOnUpdate);
+        engineFactory.setResumeOnConnect(!forceFullhandshake);
+
         // Create client
         LeshanClientBuilder builder = new LeshanClientBuilder(endpoint);
         builder.setLocalAddress(localAddress, localPort);
         builder.setObjects(enablers);
         builder.setCoapConfig(coapConfig);
         builder.setDtlsConfig(dtlsConfig);
+        builder.setRegistrationEngineFactory(engineFactory);
         if (supportOldFormat) {
             builder.setDecoder(new DefaultLwM2mNodeDecoder(true));
             builder.setEncoder(new DefaultLwM2mNodeEncoder(true));


Test patch:
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SynchronousClientObserver.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SynchronousClientObserver.java
index ebd89502..a6c96206 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SynchronousClientObserver.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/SynchronousClientObserver.java
@@ -21,10 +21,14 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.leshan.ResponseCode;
-import org.eclipse.leshan.client.observer.LwM2mClientObserver;
+import org.eclipse.leshan.client.observer.LwM2mClientObserverAdapter;
 import org.eclipse.leshan.client.servers.Server;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.DeregisterRequest;
+import org.eclipse.leshan.core.request.RegisterRequest;
+import org.eclipse.leshan.core.request.UpdateRequest;
 
-public class SynchronousClientObserver implements LwM2mClientObserver {
+public class SynchronousClientObserver extends LwM2mClientObserverAdapter {
 
     private CountDownLatch registerLatch = new CountDownLatch(1);
     private AtomicBoolean registerSucceed = new AtomicBoolean(false);
@@ -43,70 +47,74 @@ public class SynchronousClientObserver implements LwM2mClientObserver {
     private AtomicBoolean bootstrapFailed = new AtomicBoolean(false);
 
     @Override
-    public void onBootstrapSuccess(Server bsserver) {
+    public void onBootstrapSuccess(Server bsserver, BootstrapRequest request) {
         bootstrapSucceed.set(true);
         bootstrapLatch.countDown();
     }
 
     @Override
-    public void onBootstrapFailure(Server bsserver, ResponseCode responseCode, String errorMessage) {
+    public void onBootstrapFailure(Server bsserver, BootstrapRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         bootstrapFailed.set(true);
         bootstrapLatch.countDown();
     }
 
     @Override
-    public void onBootstrapTimeout(Server bsserver) {
+    public void onBootstrapTimeout(Server bsserver, BootstrapRequest request) {
         bootstrapLatch.countDown();
     }
 
     @Override
-    public void onRegistrationSuccess(Server server, String registrationID) {
+    public void onRegistrationSuccess(Server server, RegisterRequest request, String registrationID) {
         registerSucceed.set(true);
         registerLatch.countDown();
     }
 
     @Override
-    public void onRegistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onRegistrationFailure(Server server, RegisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         registerFailed.set(true);
         registerLatch.countDown();
     }
 
     @Override
-    public void onRegistrationTimeout(Server server) {
+    public void onRegistrationTimeout(Server server, RegisterRequest request) {
         registerLatch.countDown();
     }
 
     @Override
-    public void onUpdateSuccess(Server server, String registrationID) {
+    public void onUpdateSuccess(Server server, UpdateRequest request) {
         updateSucceed.set(true);
         updateLatch.countDown();
     }
 
     @Override
-    public void onUpdateFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onUpdateFailure(Server server, UpdateRequest request, ResponseCode responseCode, String errorMessage,
+            Exception cause) {
         updateFailed.set(true);
         updateLatch.countDown();
     }
 
     @Override
-    public void onUpdateTimeout(Server server) {
+    public void onUpdateTimeout(Server server, UpdateRequest request) {
         updateLatch.countDown();
     }
 
     @Override
-    public void onDeregistrationSuccess(Server server, String registrationID) {
+    public void onDeregistrationSuccess(Server server, DeregisterRequest request) {
         deregisterSucceed.set(true);
         deregisterLatch.countDown();
     }
 
     @Override
-    public void onDeregistrationFailure(Server server, ResponseCode responseCode, String errorMessage) {
+    public void onDeregistrationFailure(Server server, DeregisterRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         deregisterFailed.set(true);
         deregisterLatch.countDown();
     }
 
     @Override
-    public void onDeregistrationTimeout(Server server) {
+    public void onDeregistrationTimeout(Server server, DeregisterRequest request) {
         deregisterLatch.countDown();
     }