/** * 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.logout; import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutorService; import javax.annotation.PreDestroy; import javax.inject.Inject; import org.apache.thrift.TException; import org.diqube.connection.ConnectionException; import org.diqube.connection.ConnectionOrLocalHelper; import org.diqube.connection.ServiceProvider; import org.diqube.consensus.AbstractConsensusStateMachine; 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.consensus.ConsensusStateMachineImplementation; import org.diqube.context.InjectOptional; import org.diqube.im.callback.IdentityCallbackRegistryStateMachine; import org.diqube.im.callback.IdentityCallbackRegistryStateMachine.GetAllRegistered; import org.diqube.remote.query.TicketInfoUtil; import org.diqube.remote.query.thrift.IdentityCallbackService; import org.diqube.threads.ExecutorManager; import org.diqube.thrift.base.thrift.RNodeAddress; import org.diqube.thrift.base.thrift.Ticket; import org.diqube.ticket.TicketValidityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.atomix.copycat.server.Commit; /** * Implementation of {@link LogoutStateMachine} * * @author Bastian Gloeckle */ @ConsensusStateMachineImplementation public class LogoutStateMachineImplementation extends AbstractConsensusStateMachine<Ticket> implements LogoutStateMachine { private static final Logger logger = LoggerFactory.getLogger(LogoutStateMachineImplementation.class); private static final String INTERNALDB_FILE_PREFIX = "logout-"; private static final String INTERNALDB_DATA_TYPE = "logout_v1"; private Set<Ticket> invalidTickets = new ConcurrentSkipListSet<>(); private Map<Ticket, Commit<?>> previousCommit = new ConcurrentHashMap<>(); @Inject private TicketValidityService ticketValidityService; @Inject private ConnectionOrLocalHelper connectionOrLocalHelper; @Inject private ConsensusClient consensusClient; @InjectOptional private List<LogoutStateMachineListener> listeners; @Inject private ExecutorManager executorManager; private ExecutorService executorService; public LogoutStateMachineImplementation() { super(INTERNALDB_FILE_PREFIX, INTERNALDB_DATA_TYPE, () -> new Ticket()); } @Override protected void doInitialize(List<Ticket> entriesLoadedFromInternalDb) { executorService = executorManager.newCachedThreadPoolWithMax("logout-state-machine-async-worker-%d", new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // Log, but ignore otherwise - at least one cluster node should succeed. And if not: We did not guarantee // that // we'd reach all registered callbacks! Usually we should succeed though. logger.warn("Failed to execute async work in {}", LogoutStateMachineImplementation.class.getSimpleName(), e); } }, 5); if (entriesLoadedFromInternalDb != null) invalidTickets.addAll(entriesLoadedFromInternalDb); } @PreDestroy public void cleanup() { if (executorService != null) executorService.shutdownNow(); } @Override public void logout(Commit<Logout> commit) { Ticket t = commit.operation().getTicket(); Commit<?> prev = previousCommit.put(t, commit); ticketValidityService.markTicketAsInvalid(TicketInfoUtil.fromTicket(t)); invalidTickets.add(t); writeCurrentStateToInternalDb(commit.index(), invalidTickets); // inform all registered callbacks, but do this asynchronously. This is needed since we use a consensus client here // again which might conenct to the local node: We would end up in a deadlock, since the local consensus server is // executign something already. It is not vital that the callbacks are called synchrounously anyway. executorService.execute(() -> { Set<RNodeAddress> callbackAddresses = new HashSet<>(); try (ClosableProvider<IdentityCallbackRegistryStateMachine> p = consensusClient.getStateMachineClient(IdentityCallbackRegistryStateMachine.class)) { List<RNodeAddress> addrs = p.getClient().getAllRegistered(GetAllRegistered.local()); addrs.forEach(a -> callbackAddresses.add(a)); } catch (ConsensusClusterUnavailableException e) { logger.warn("Could not get adresses of logout callbacks from consensus cluster. Will not inform any.", e); } catch (ConsensusStateMachineClientInterruptedException e) { // quietly exit return; } catch (RuntimeException e) { logger.warn("Could not get addresses of logout callbacks from consensus cluster. Will not inform any.", e); return; } // note that here again we might not reach all registered callbacks (e.g. because of network partitions). The // callbacks must poll a fresh list of invalidated tickets periodically and should not accept any tickets if the // can't reach the cluster. for (RNodeAddress callbackAddr : callbackAddresses) { try (ServiceProvider<IdentityCallbackService.Iface> p = connectionOrLocalHelper.getService(IdentityCallbackService.Iface.class, callbackAddr, null)) { p.getService().ticketBecameInvalid(TicketInfoUtil.fromTicket(t)); } catch (InterruptedException e) { throw new RuntimeException("Interrupted while communicating to " + callbackAddr, e); } catch (TException | IOException | ConnectionException | RuntimeException e) { logger.warn("Could not send invalidation of ticket (logout) to registered callback node {}.", callbackAddr); } } }); if (prev != null) prev.close(); if (listeners != null) listeners.forEach(l -> l.ticketBecameInvalid(t)); } @Override public List<Ticket> getInvalidTickets(Commit<GetInvalidTickets> commit) { return new ArrayList<>(invalidTickets); } @Override public void cleanLogoutTicket(Commit<CleanLogoutTicket> commit) { Ticket t = commit.operation().getTicket(); Commit<?> prev = previousCommit.remove(t); invalidTickets.remove(t); writeCurrentStateToInternalDb(commit.index(), invalidTickets); if (prev != null) prev.close(); commit.close(); } /** * Simple listener that is informed about tickets that became invalid (commited ones) */ /* package */static interface LogoutStateMachineListener { public void ticketBecameInvalid(Ticket t); } }