/** * diqube: Distributed Query Base. * * Copyright (C) 2015 Bastian Gloeckle * * This file is part of diqube. * * diqube is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.diqube.im.callback; import java.time.Instant; import java.util.Deque; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; import java.util.Set; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ThreadLocalRandom; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import org.diqube.consensus.ConsensusClient; import org.diqube.consensus.ConsensusClient.ClosableProvider; import org.diqube.consensus.ConsensusClient.ConsensusClusterUnavailableException; import org.diqube.consensus.ConsensusStateMachineClientInterruptedException; import org.diqube.context.AutoInstatiate; import org.diqube.im.callback.IdentityCallbackRegistryStateMachine.Unregister; import org.diqube.im.callback.IdentityCallbackRegistryStateMachineImplementation.IdentityCallbackRegistryListener; import org.diqube.thrift.base.thrift.RNodeAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Cleans entries from {@link IdentityCallbackRegistryStateMachine} that are older than * {@link #CALLBACK_REGISTER_TIMEOUT_MIN}. * * @author Bastian Gloeckle */ @AutoInstatiate public class IdentityCallbackRegistryCleaner implements IdentityCallbackRegistryListener { private static final Logger logger = LoggerFactory.getLogger(IdentityCallbackRegistryCleaner.class); /** max. seconds after which {@link IdentityCallbackRegistryStateMachineImplementation} is polled and cleaned */ public static final long MAX_POLL_SEC = 120; /** Timeout of a "registerCallback" call after which the callback is deregistered by this cleaner. */ public static final long CALLBACK_REGISTER_TIMEOUT_MIN = 1 * 60; // 1h @Inject private IdentityCallbackRegistryStateMachineImplementation registry; @Inject private ConsensusClient consensusClient; private CheckThread thread; private NavigableMap<Long, Deque<RNodeAddress>> registeredTimes = new ConcurrentSkipListMap<>(); @PostConstruct public void initialize() { thread = new CheckThread(); thread.start(); } @PreDestroy public void cleanup() { thread.interrupt(); } @Override public void registered(RNodeAddress callbackNode, long registerTime) { registeredTimes.compute(registerTime, (key, oldValue) -> { Deque<RNodeAddress> res = new ConcurrentLinkedDeque<>(); res.addLast(callbackNode); if (oldValue != null) res.addAll(oldValue); return res; }); } @Override public void unregistered(RNodeAddress callbackNode, long lastRegisterTime) { registeredTimes.computeIfPresent(lastRegisterTime, (key, value) -> { Deque<RNodeAddress> res = new ConcurrentLinkedDeque<>(); res.addAll(value); res.remove(callbackNode); if (res.isEmpty()) return null; return res; }); } private class CheckThread extends Thread { /* package */ CheckThread() { super("identity-callback-registry-cleaner"); } @Override public void run() { long randomWaitDiff = ThreadLocalRandom.current().nextLong((MAX_POLL_SEC / 2) * 1_000L); while (true) { try { Thread.sleep((MAX_POLL_SEC / 2) * 1_000L + randomWaitDiff); // Random this that not all nodes execute this // simultaneously. } catch (InterruptedException e) { // exit quietly. return; } Set<RNodeAddress> callbackNodesToUnregister = new HashSet<>(); long timeoutTime = System.currentTimeMillis() - CALLBACK_REGISTER_TIMEOUT_MIN * 60 * 1_000L; Map<Long, Deque<RNodeAddress>> timeouts = registeredTimes.headMap(timeoutTime); for (Entry<Long, Deque<RNodeAddress>> e : timeouts.entrySet()) { long registerTime = e.getKey(); for (RNodeAddress callbackNode : e.getValue()) { Long currentRegisterTime = registry.getCurrentRegisterTime(callbackNode); // check if node is unregistered already if (currentRegisterTime == null) continue; // check if node was not re-registered already. if (currentRegisterTime > registerTime) continue; callbackNodesToUnregister.add(callbackNode); } } if (!callbackNodesToUnregister.isEmpty()) { logger.info( "Will unregister following {} nodes from receiving any calls to their IdentityCallbackService," + " because they did not re-register soon enough (registered before {}): {}", callbackNodesToUnregister.size(), Instant.ofEpochMilli(timeoutTime), callbackNodesToUnregister); try (ClosableProvider<IdentityCallbackRegistryStateMachine> p = consensusClient.getStateMachineClient(IdentityCallbackRegistryStateMachine.class)) { for (RNodeAddress addr : callbackNodesToUnregister) p.getClient().unregister(Unregister.local(addr)); } catch (ConsensusClusterUnavailableException e) { logger.warn("Could not execute unregister calls since consensus cluster to find " + "addresses is unavailable. Ignoring.", e); } catch (ConsensusStateMachineClientInterruptedException e) { // interrupted, exit quietly. return; } catch (RuntimeException e) { logger.warn("Could not exeucte unregister calls. Ignoring", e); } } } } } }