package cc.blynk.server.workers.timer;
import cc.blynk.server.core.dao.SessionDao;
import cc.blynk.server.core.dao.UserDao;
import cc.blynk.server.core.dao.UserKey;
import cc.blynk.server.core.model.DashBoard;
import cc.blynk.server.core.model.auth.Session;
import cc.blynk.server.core.model.auth.User;
import cc.blynk.server.core.model.widgets.Target;
import cc.blynk.server.core.model.widgets.Widget;
import cc.blynk.server.core.model.widgets.controls.Timer;
import cc.blynk.server.core.model.widgets.others.eventor.Eventor;
import cc.blynk.server.core.model.widgets.others.eventor.Rule;
import cc.blynk.server.core.model.widgets.others.eventor.TimerTime;
import cc.blynk.server.core.model.widgets.others.eventor.model.action.BaseAction;
import cc.blynk.server.core.model.widgets.others.eventor.model.action.SetPinAction;
import cc.blynk.server.core.model.widgets.others.eventor.model.action.notification.NotifyAction;
import cc.blynk.server.core.processors.EventorProcessor;
import cc.blynk.server.notifications.push.GCMWrapper;
import cc.blynk.utils.ArrayUtil;
import cc.blynk.utils.DateTimeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static cc.blynk.server.core.protocol.enums.Command.HARDWARE;
/**
* Timer worker class responsible for triggering all timers at specified time.
* Current implementation is some kind of Hashed Wheel Timer.
* In general idea is very simple :
*
* Select timers at specified cell timer[secondsOfDayNow]
* and run it one by one, instead of naive implementation
* with iteration over all profiles every second
*
* + Concurrency around it as timerWorker may be accessed from different threads.
*
* The Blynk Project.
* Created by Dmitriy Dumanskiy.
* Created on 2/6/2015.
*
*/
public class TimerWorker implements Runnable {
private static final Logger log = LogManager.getLogger(TimerWorker.class);
public static final int TIMER_MSG_ID = 7777;
private final UserDao userDao;
private final SessionDao sessionDao;
private final GCMWrapper gcmWrapper;
private final ConcurrentMap<TimerKey, BaseAction[]>[] timerExecutors;
private final static int size = 8640;
@SuppressWarnings("unchecked")
public TimerWorker(UserDao userDao, SessionDao sessionDao, GCMWrapper gcmWrapper) {
this.userDao = userDao;
this.sessionDao = sessionDao;
this.gcmWrapper = gcmWrapper;
//array cell for every second in a day,
//yes, it costs a bit of memory, but still cheap :)
this.timerExecutors = new ConcurrentMap[size];
for (int i = 0; i < size; i++) {
timerExecutors[i] = new ConcurrentHashMap<>();
}
init(userDao.users);
}
private static int hash(int time) {
return time / 10;
}
private void init(ConcurrentMap<UserKey, User> users) {
int counter = 0;
for (Map.Entry<UserKey, User> entry : users.entrySet()) {
for (DashBoard dashBoard : entry.getValue().profile.dashBoards) {
for (Widget widget : dashBoard.widgets) {
if (widget instanceof Timer) {
Timer timer = (Timer) widget;
add(entry.getKey(), timer, dashBoard.id);
counter++;
}
if (widget instanceof Eventor) {
Eventor eventor = (Eventor) widget;
add(entry.getKey(), eventor, dashBoard.id);
counter++;
}
}
}
}
log.info("Timers : {}", counter);
}
public void add(UserKey userKey, Eventor eventor, int dashId) {
if (eventor.rules != null) {
for (Rule rule : eventor.rules) {
if (rule.isValidTimerRule()) {
add(userKey, dashId, eventor.deviceId, eventor.id,
rule.triggerTime.id, rule.triggerTime, rule.actions);
}
}
}
}
public void add(UserKey userKey, Timer timer, int dashId) {
if (timer.isValidStart()) {
add(userKey, dashId, timer.deviceId, timer.id, 0, new TimerTime(timer.startTime), new SetPinAction(timer.pin, timer.pinType, timer.startValue));
}
if (timer.isValidStop()) {
add(userKey, dashId, timer.deviceId, timer.id, 1, new TimerTime(timer.stopTime), new SetPinAction(timer.pin, timer.pinType, timer.stopValue));
}
}
private void add(UserKey userKey, int dashId, int deviceId, long widgetId, int additionalId, TimerTime time, BaseAction[] actions) {
ArrayList<BaseAction> validActions = new ArrayList<>(actions.length);
for (BaseAction action : actions) {
if (action.isValid()) {
validActions.add(action);
}
}
if (!validActions.isEmpty()) {
timerExecutors[hash(time.time)].put(new TimerKey(userKey, dashId, deviceId, widgetId, additionalId, time), validActions.toArray(new BaseAction[validActions.size()]));
}
}
private void add(UserKey userKey, int dashId, int deviceId, long widgetId, int additionalId, TimerTime time, BaseAction action) {
if (action.isValid()) {
timerExecutors[hash(time.time)].put(new TimerKey(userKey, dashId, deviceId, widgetId, additionalId, time), new BaseAction[]{action});
}
}
public void delete(UserKey userKey, Eventor eventor, int dashId) {
if (eventor.rules != null) {
for (Rule rule : eventor.rules) {
if (rule.isValidTimerRule()) {
delete(userKey, dashId, eventor.deviceId, eventor.id, rule.triggerTime.id, rule.triggerTime);
}
}
}
}
public void delete(UserKey userKey, Timer timer, int dashId) {
if (timer.isValidStart()) {
delete(userKey, dashId, timer.deviceId, timer.id, 0, new TimerTime(timer.startTime));
}
if (timer.isValidStop()) {
delete(userKey, dashId, timer.deviceId, timer.id, 1, new TimerTime(timer.stopTime));
}
}
private void delete(UserKey userKey, int dashId, int deviceId, long widgetId, int additionalId, TimerTime time) {
timerExecutors[hash(time.time)].remove(new TimerKey(userKey, dashId, deviceId, widgetId, additionalId, time));
}
private int actuallySendTimers;
@Override
public void run() {
log.trace("Starting timer...");
final ZonedDateTime currentDateTime = ZonedDateTime.now(DateTimeUtils.UTC);
final int curSeconds = currentDateTime.toLocalTime().toSecondOfDay();
ConcurrentMap<TimerKey, BaseAction[]> tickedExecutors = timerExecutors[hash(curSeconds)];
int readyForTickTimers = tickedExecutors.size();
if (readyForTickTimers == 0) {
return;
}
final long now = System.currentTimeMillis();
int activeTimers = 0;
try {
activeTimers = send(tickedExecutors, currentDateTime, curSeconds, now);
} catch (Exception e) {
log.error("Error running timers. ", e);
}
if (activeTimers > 0) {
log.info("Timer finished. Ready {}, Active {}, Actual {}. Processing time : {} ms",
readyForTickTimers, activeTimers, actuallySendTimers, System.currentTimeMillis() - now);
}
}
private int send(ConcurrentMap<TimerKey, BaseAction[]> tickedExecutors, ZonedDateTime currentDateTime, int curSeconds, long now) {
int activeTimers = 0;
actuallySendTimers = 0;
for (Map.Entry<TimerKey, BaseAction[]> entry : tickedExecutors.entrySet()) {
final TimerKey key = entry.getKey();
final BaseAction[] actions = entry.getValue();
if (key.time.time == curSeconds && isTime(key.time, currentDateTime)) {
User user = userDao.users.get(key.userKey);
if (user != null) {
DashBoard dash = user.profile.getDashById(key.dashId);
if (dash != null && dash.isActive) {
activeTimers++;
process(dash, key, actions, now);
}
}
}
}
return activeTimers;
}
private void process(DashBoard dash, TimerKey key, BaseAction[] actions, long now) {
for (BaseAction action : actions) {
if (action instanceof SetPinAction) {
SetPinAction setPinAction = (SetPinAction) action;
Target target = dash.getTarget(key.deviceId);
if (target == null) {
return;
}
int[] deviceIds = target.getDeviceIds();
if (deviceIds.length == 0) {
return;
}
for (int deviceId : deviceIds) {
dash.update(deviceId, setPinAction.pin.pin, setPinAction.pin.pinType, setPinAction.value, now);
}
triggerTimer(sessionDao, key.userKey, setPinAction.makeHardwareBody(), key.dashId, deviceIds);
} else if (action instanceof NotifyAction) {
NotifyAction notifyAction = (NotifyAction) action;
EventorProcessor.push(gcmWrapper, dash, notifyAction.message);
}
//todo other type of actions not supported yet. maybe in future.
}
}
private boolean isTime(TimerTime timerTime, ZonedDateTime currentDateTime) {
LocalDateTime userDateTime = currentDateTime.withZoneSameInstant(timerTime.tzName).toLocalDateTime();
final int dayOfWeek = userDateTime.getDayOfWeek().ordinal() + 1;
return ArrayUtil.contains(timerTime.days, dayOfWeek);
}
private void triggerTimer(SessionDao sessionDao, UserKey userKey, String value, int dashId, int[] deviceIds) {
Session session = sessionDao.userSession.get(userKey);
if (session != null) {
if (!session.sendMessageToHardware(dashId, HARDWARE, TIMER_MSG_ID, value, deviceIds)) {
actuallySendTimers++;
}
for (int deviceId : deviceIds) {
session.sendToApps(HARDWARE, TIMER_MSG_ID, dashId, deviceId, value);
}
}
}
}