/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.binding.lifx.internal; import static org.eclipse.smarthome.binding.lifx.LifxBindingConstants.PACKET_INTERVAL; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.smarthome.binding.lifx.internal.fields.MACAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link LifxNetworkThrottler} is a helper class that regulates the frequency at which messages/packets are sent to * LIFX lights. The LIFX LAN Protocol Specification states that lights can process up to 20 messages per second, not * more. * * @author Karel Goderis - Initial Contribution * @author Wouter Born - Deadlock fix */ public class LifxNetworkThrottler { private static Logger logger = LoggerFactory.getLogger(LifxNetworkThrottler.class); /** * Tracks when the last packet was sent to a LIFX light. The packet is sent after obtaining the lock and before * releasing the lock. */ private static class LifxLightCommunicationTracker { private long timestamp; private ReentrantLock lock = new ReentrantLock(); public void lock() { lock.lock(); } public void unlock() { // When iterating over all trackers another thread may have inserted this object so this thread may not // have a lock on it. When the thread does not have the lock, it also did not send a packet. if (lock.isHeldByCurrentThread()) { timestamp = System.currentTimeMillis(); lock.unlock(); } } public long getTimestamp() { return timestamp; } } /** * A separate list of trackers is maintained when locking all lights in case of a broadcast. Iterators of * {@link ConcurrentHashMap}s may behave non-linear when inserts take place to obtain more concurrency. When the * iterator of {@code values()} of {@link #macTrackerMapping} is used for locking all lights, it could sometimes * cause deadlock. */ private static List<LifxLightCommunicationTracker> trackers = new CopyOnWriteArrayList<>(); private static Map<MACAddress, LifxLightCommunicationTracker> macTrackerMapping = new ConcurrentHashMap<MACAddress, LifxLightCommunicationTracker>(); public static void lock(MACAddress mac) { LifxLightCommunicationTracker tracker = getOrCreateTracker(mac); tracker.lock(); waitForNextPacketInterval(tracker.getTimestamp()); } private static LifxLightCommunicationTracker getOrCreateTracker(MACAddress mac) { LifxLightCommunicationTracker tracker = macTrackerMapping.get(mac); if (tracker == null) { // for better performance only synchronize when necessary synchronized (trackers) { // another thread may just have added a tracker in this synchronized block, so reevaluate tracker = macTrackerMapping.get(mac); if (tracker == null) { tracker = new LifxLightCommunicationTracker(); trackers.add(tracker); macTrackerMapping.put(mac, tracker); } } } return tracker; } private static void waitForNextPacketInterval(long timestamp) { long timeToWait = Math.max(PACKET_INTERVAL - (System.currentTimeMillis() - timestamp), 0); if (timeToWait > 0) { try { Thread.sleep(timeToWait); } catch (InterruptedException e) { logger.error("An exception occurred while putting the thread to sleep : '{}'", e.getMessage()); } } } public static void unlock(MACAddress mac) { if (macTrackerMapping.containsKey(mac)) { macTrackerMapping.get(mac).unlock(); } } public static void lock() { long lastStamp = 0; for (LifxLightCommunicationTracker tracker : trackers) { tracker.lock(); lastStamp = Math.max(lastStamp, tracker.getTimestamp()); } waitForNextPacketInterval(lastStamp); } public static void unlock() { for (LifxLightCommunicationTracker tracker : trackers) { tracker.unlock(); } } }