All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.leshan.client.servers.RegistrationEngine Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright (c) 2015 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.client.servers;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.eclipse.leshan.LwM2m;
import org.eclipse.leshan.ResponseCode;
import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.request.LwM2mRequestSender;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.util.LinkFormatHelper;
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;
import org.eclipse.leshan.core.request.exception.SendFailedException;
import org.eclipse.leshan.core.response.BootstrapResponse;
import org.eclipse.leshan.core.response.DeregisterResponse;
import org.eclipse.leshan.core.response.RegisterResponse;
import org.eclipse.leshan.core.response.UpdateResponse;
import org.eclipse.leshan.util.NamedThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manage the registration life-cycle:
 * 
    *
  • Start bootstrap session if no device Management server is available
  • *
  • Register to device management server when available (at startup or after bootstrap)
  • *
  • Update registration periodically.
  • *
  • If communication failed with device management server, try to bootstrap again each 10 minutes until succeed
  • *
*
* For now support only one device management server. */ public class RegistrationEngine { private static final Logger LOG = LoggerFactory.getLogger(RegistrationEngine.class); // 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. private static final long DEFAULT_TIMEOUT = 2 * 60 * 1000l; // 2min in ms // TODO bootstrap timeout should be configurable private static final int BS_TIMEOUT = 93; // in seconds (93s is the COAP MAX_TRANSMIT_WAIT with default config) private static final long DEREGISTRATION_TIMEOUT = 1000; // in ms, de-registration is only used on stop for now. // TODO time between bootstrap retry should be configurable and incremental private static final int BS_RETRY = 10 * 60 * 1000; // in ms private static final long NOW = 0; private static enum Status { SUCCESS, FAILURE, TIMEOUT } // device state private final String endpoint; private final Map additionalAttributes; private final Map objectEnablers; private final Map registeredServers; // helpers private final LwM2mRequestSender sender; private final BootstrapHandler bootstrapHandler; private final EndpointsManager endpointsManager; private final LwM2mClientObserver observer; // tasks stuff private boolean started = false; private Future bootstrapFuture; private Future registerFuture; private Future updateFuture; private final ScheduledExecutorService schedExecutor = Executors .newSingleThreadScheduledExecutor(new NamedThreadFactory("RegistrationEngine#%d")); public RegistrationEngine(String endpoint, Map objectEnablers, EndpointsManager endpointsManager, LwM2mRequestSender requestSender, BootstrapHandler bootstrapState, LwM2mClientObserver observer, Map additionalAttributes) { this.endpoint = endpoint; this.objectEnablers = objectEnablers; this.bootstrapHandler = bootstrapState; this.endpointsManager = endpointsManager; this.observer = observer; this.additionalAttributes = additionalAttributes; this.registeredServers = new ConcurrentHashMap<>(); sender = requestSender; } public void start() { stop(false); // Stop without de-register synchronized (this) { started = true; // Try factory bootstrap Collection dmServers = factoryBootstrap(); if (dmServers == null || dmServers.isEmpty()) { // If it failed try client initiated bootstrap if (!scheduleClientInitiatedBootstrap(NOW)) throw new IllegalStateException("Unable to start client : No valid server available!"); } else { // Try to register to dm servers. // TODO we currently support only one dm server. Server dmServer = dmServers.iterator().next(); registerFuture = schedExecutor.submit(new RegistrationTask(dmServer)); } } } public Collection factoryBootstrap() { ServersInfo serversInfo = ServersInfoExtractor.getInfo(objectEnablers); if (!serversInfo.deviceMangements.isEmpty()) { Collection servers = endpointsManager.createEndpoints(serversInfo.deviceMangements.values()); return servers; } return null; } private Collection clientInitiatedBootstrap() throws InterruptedException { ServerInfo bootstrapServerInfo = ServersInfoExtractor.getBootstrapServerInfo(objectEnablers); if (bootstrapServerInfo == null) { LOG.error("Trying to bootstrap device but there is no bootstrap server config."); return null; } if (bootstrapHandler.tryToInitSession(bootstrapServerInfo)) { LOG.info("Trying to start bootstrap session to {} ...", bootstrapServerInfo.getFullUri()); // Clear all registered server, cancel all current task and recreate all endpoints registeredServers.clear(); cancelRegistrationTask(); cancelUpdateTask(true); Server bootstrapServer = endpointsManager.createEndpoint(bootstrapServerInfo); // Send bootstrap request try { BootstrapResponse response = sender.send(bootstrapServerInfo.getAddress(), bootstrapServerInfo.isSecure(), new BootstrapRequest(endpoint), DEFAULT_TIMEOUT); if (response == null) { LOG.error("Unable to start bootstrap session: Timeout."); if (observer != null) { observer.onBootstrapTimeout(bootstrapServer); } return null; } else if (response.isSuccess()) { LOG.info("Bootstrap started"); // Wait until it is finished (or too late) boolean timeout = !bootstrapHandler.waitBoostrapFinished(BS_TIMEOUT); if (timeout) { LOG.error("Bootstrap sequence aborted: Timeout."); if (observer != null) { observer.onBootstrapTimeout(bootstrapServer); } return null; } else { LOG.info("Bootstrap finished {}.", bootstrapServerInfo); ServersInfo serverInfos = ServersInfoExtractor.getInfo(objectEnablers); Collection dmServers = null; if (!serverInfos.deviceMangements.isEmpty()) { dmServers = endpointsManager.createEndpoints(serverInfos.deviceMangements.values()); } if (observer != null) { observer.onBootstrapSuccess(bootstrapServer); } return dmServers; } } else { LOG.error("Bootstrap failed: {} {}.", response.getCode(), response.getErrorMessage()); if (observer != null) { observer.onBootstrapFailure(bootstrapServer, response.getCode(), response.getErrorMessage()); } return null; } } catch (SendFailedException e) { logExceptionOnSendRequest("Unable to send Bootstrap request", e); return null; } finally { bootstrapHandler.closeSession(); } } else { LOG.warn("Bootstrap sequence already started."); return null; } } private boolean registerWithRetry(Server server) throws InterruptedException { Status registerStatus = register(server); if (registerStatus == Status.TIMEOUT) { // if register timeout maybe server lost the session, // so we reconnect (new handshake) and retry endpointsManager.forceReconnection(server); registerStatus = register(server); } return registerStatus == Status.SUCCESS; } private Status register(Server server) throws InterruptedException { DmServerInfo dmInfo = ServersInfoExtractor.getDMServerInfo(objectEnablers, server.getId()); if (dmInfo == null) { LOG.error("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()); 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, DEFAULT_TIMEOUT); if (response == null) { LOG.error("Registration failed: Timeout."); if (observer != null) { observer.onRegistrationTimeout(server); } return Status.TIMEOUT; } else if (response.isSuccess()) { // Add server to registered one String registrationID = response.getRegistrationID(); registeredServers.put(registrationID, server); LOG.info("Registered with location '{}'.", registrationID); // Update every lifetime period long delay = calculateNextUpdate(dmInfo.lifetime); scheduleUpdate(server, registrationID, delay); if (observer != null) { observer.onRegistrationSuccess(server, registrationID); } return Status.SUCCESS; } else { LOG.error("Registration failed: {} {}.", response.getCode(), response.getErrorMessage()); if (observer != null) { observer.onRegistrationFailure(server, response.getCode(), response.getErrorMessage()); } return Status.FAILURE; } } catch (SendFailedException e) { logExceptionOnSendRequest("Unable to send register request", e); return Status.FAILURE; } } private boolean deregister(Server server, String registrationID) throws InterruptedException { if (registrationID == null) return true; DmServerInfo dmInfo = ServersInfoExtractor.getDMServerInfo(objectEnablers, server.getId()); if (dmInfo == null) { LOG.error("Trying to deregister device but there is no LWM2M server config."); return false; } // Send deregister request LOG.info("Trying to deregister to {} ...", server.getUri()); try { DeregisterResponse response = sender.send(server.getIdentity().getPeerAddress(), server.getIdentity().isSecure(), new DeregisterRequest(registrationID), DEREGISTRATION_TIMEOUT); if (response == null) { registrationID = null; LOG.error("Deregistration failed: Timeout."); if (observer != null) { observer.onDeregistrationTimeout(server); } return false; } else if (response.isSuccess() || response.getCode() == ResponseCode.NOT_FOUND) { registeredServers.remove(registrationID); registrationID = null; cancelUpdateTask(true); LOG.info("De-register response {} {}.", response.getCode(), response.getErrorMessage()); if (observer != null) { if (response.isSuccess()) { observer.onDeregistrationSuccess(server, registrationID); } else { observer.onDeregistrationFailure(server, response.getCode(), response.getErrorMessage()); } } return true; } else { LOG.error("Deregistration failed: {} {}.", response.getCode(), response.getErrorMessage()); if (observer != null) { observer.onDeregistrationFailure(server, response.getCode(), response.getErrorMessage()); } return false; } } catch (SendFailedException e) { logExceptionOnSendRequest("Unable to send deregister request", e); return false; } } private boolean updateWithRetry(Server server, String registrationId) throws InterruptedException { Status updateStatus = update(server, registrationId); if (updateStatus == Status.TIMEOUT) { // if register timeout maybe server lost the session, // so we reconnect (new handshake) and retry endpointsManager.forceReconnection(server); updateStatus = update(server, registrationId); } return updateStatus == Status.SUCCESS; } private Status update(Server server, String registrationID) 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."); return Status.FAILURE; } // Send update LOG.info("Trying to update registration to {} ...", server.getUri()); try { UpdateResponse response = sender.send(dmInfo.getAddress(), dmInfo.isSecure(), new UpdateRequest(registrationID, null, null, null, null, null), DEFAULT_TIMEOUT); if (response == null) { registrationID = null; LOG.error("Registration update failed: Timeout."); if (observer != null) { observer.onUpdateTimeout(server); } 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); scheduleUpdate(server, registrationID, delay); if (observer != null) { observer.onUpdateSuccess(server, registrationID); } return Status.SUCCESS; } else { LOG.error("Registration update failed: {} {}.", response.getCode(), response.getErrorMessage()); if (observer != null) { observer.onUpdateFailure(server, response.getCode(), response.getErrorMessage()); } registeredServers.remove(registrationID); return Status.FAILURE; } } catch (SendFailedException e) { logExceptionOnSendRequest("Unable to send update request", 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 synchronized boolean scheduleClientInitiatedBootstrap(long timeInMs) { if (!started) return false; ServerInfo bootstrapServerInfo = ServersInfoExtractor.getBootstrapServerInfo(objectEnablers); if (bootstrapServerInfo == null) { // It seems we have no bootstrap server available in this case we can't schedule a new bootstraps return false; } // Schedule a client initiated bootstrap only if there is not already one scheduled or in execution if (bootstrapFuture == null || bootstrapFuture.isDone() || bootstrapFuture.isCancelled()) { if (timeInMs > 0) { LOG.info("Try to initiated bootstarp in {}s...", timeInMs / 1000); bootstrapFuture = schedExecutor.schedule(new ClientInitiatedBootstrapTask(), timeInMs, TimeUnit.MILLISECONDS); } else { bootstrapFuture = schedExecutor.submit(new ClientInitiatedBootstrapTask()); } } // We succeed to schedule a bootstrap or there is already one schedule so it's ok. return true; } private class ClientInitiatedBootstrapTask implements Runnable { @Override public void run() { try { Collection dmServers = clientInitiatedBootstrap(); if (dmServers == null || dmServers.isEmpty()) { scheduleClientInitiatedBootstrap(BS_RETRY); } else { Server dmServer = dmServers.iterator().next(); if (!registerWithRetry(dmServer)) scheduleRegistrationTask(dmServer, BS_RETRY); } } catch (InterruptedException e) { LOG.info("Bootstrap task interrupted. "); } catch (RuntimeException e) { LOG.error("Unexpected exception during bootstrap task", e); } } } private synchronized void scheduleRegistrationTask(Server dmServer, long timeInMs) { if (!started) return; if (timeInMs > 0) { LOG.info("Try to register to {} again in {}s...", dmServer.getUri(), timeInMs / 1000); registerFuture = schedExecutor.schedule(new RegistrationTask(dmServer), timeInMs, TimeUnit.MILLISECONDS); } else { registerFuture = schedExecutor.submit(new RegistrationTask(dmServer)); } return; } private class RegistrationTask implements Runnable { private final Server server; public RegistrationTask(Server server) { this.server = server; } @Override public void run() { try { if (!registerWithRetry(server)) { if (!scheduleClientInitiatedBootstrap(NOW)) { scheduleRegistrationTask(server, BS_RETRY); } } } catch (InterruptedException e) { LOG.info("Registration task interrupted. "); } catch (RuntimeException e) { LOG.error("Unexpected exception during registration task", e); } } } private synchronized void scheduleUpdate(Server server, String registrationId, long timeInMs) { if (!started) return; if (timeInMs > 0) { LOG.info("Next registration update to {} in {}s...", server.getUri(), timeInMs / 1000); updateFuture = schedExecutor.schedule(new UpdateRegistrationTask(server, registrationId), timeInMs, TimeUnit.MILLISECONDS); } else { updateFuture = schedExecutor.submit(new UpdateRegistrationTask(server, registrationId)); } } private class UpdateRegistrationTask implements Runnable { private Server server; private final String registrationId; public UpdateRegistrationTask(Server server, String registrationId) { this.server = server; this.registrationId = registrationId; } @Override public void run() { try { if (!updateWithRetry(server, registrationId)) { if (!registerWithRetry(server)) { if (!scheduleClientInitiatedBootstrap(NOW)) { scheduleRegistrationTask(server, BS_RETRY); } } } } catch (InterruptedException e) { LOG.info("Registration update task interrupted."); } catch (RuntimeException e) { LOG.error("Unexpected exception during update registration task", e); } } } private void cancelUpdateTask(boolean mayinterrupt) { if (updateFuture != null) { updateFuture.cancel(mayinterrupt); } } private void cancelRegistrationTask() { if (registerFuture != null) { registerFuture.cancel(true); } } private void cancelBootstrapTask() { if (bootstrapFuture != null) { bootstrapFuture.cancel(true); } } public void stop(boolean deregister) { synchronized (this) { if (!started) return; started = false; cancelUpdateTask(true); cancelRegistrationTask(); // TODO we should manage the case where we stop in the middle of a bootstrap session ... cancelBootstrapTask(); } try { if (deregister) { // TODO we currently support only one dm server. if (!registeredServers.isEmpty()) { Entry currentServer = registeredServers.entrySet().iterator().next(); deregister(currentServer.getValue(), currentServer.getKey()); } } } catch (InterruptedException e) { } } public void destroy(boolean deregister) { boolean wasStarted = false; synchronized (this) { wasStarted = started; started = false; } // TODO we should manage the case where we stop in the middle of a bootstrap session ... schedExecutor.shutdownNow(); try { schedExecutor.awaitTermination(BS_TIMEOUT, TimeUnit.SECONDS); if (wasStarted && deregister) { // TODO we currently support only one dm server. if (!registeredServers.isEmpty()) { Entry currentServer = registeredServers.entrySet().iterator().next(); deregister(currentServer.getValue(), currentServer.getKey()); } } } catch (InterruptedException e) { } } public void triggerRegistrationUpdate() { synchronized (this) { if (started) { LOG.info("Triggering registration update..."); if (registeredServers.isEmpty()) { LOG.info("No server registered!"); } else { cancelUpdateTask(true); // TODO we currently support only one dm server. Entry currentServer = registeredServers.entrySet().iterator().next(); scheduleUpdate(currentServer.getValue(), currentServer.getKey(), NOW); } } } } private void logExceptionOnSendRequest(String message, Exception e) { if (LOG.isDebugEnabled()) { LOG.warn(message, e); return; } if (e instanceof SendFailedException) { if (e.getCause() != null && e.getMessage() != null) { LOG.warn("{} : {}", message, e.getCause().getMessage()); return; } } LOG.warn("{} : {}", message, e.getMessage()); } /** * @return the current registration Id or null if the client is not registered. */ public String getRegistrationId() { // TODO we currently support only one dm server. Iterator it = registeredServers.keySet().iterator(); if (it.hasNext()) { return it.next(); } return null; } /** * @return the LWM2M client endpoint identifier. */ public String getEndpoint() { return endpoint; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy