// Copyright 2017 JanusGraph Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.janusgraph.graphdb.database.management; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import org.janusgraph.core.JanusGraphTransaction; import org.janusgraph.diskstorage.ResourceUnavailableException; import org.janusgraph.diskstorage.util.time.Timer; import org.janusgraph.diskstorage.util.time.TimestampProvider; import org.janusgraph.diskstorage.ReadBuffer; import org.janusgraph.diskstorage.log.Log; import org.janusgraph.diskstorage.log.Message; import org.janusgraph.diskstorage.log.MessageReader; import org.janusgraph.graphdb.database.StandardJanusGraph; import org.janusgraph.graphdb.database.cache.SchemaCache; import org.janusgraph.graphdb.database.idhandling.VariableLong; import org.janusgraph.graphdb.database.serialize.DataOutput; import org.janusgraph.graphdb.database.serialize.Serializer; import org.janusgraph.graphdb.types.vertices.JanusGraphSchemaVertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.Iterator; import java.util.Set; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** * @author Matthias Broecheler (me@matthiasb.com) */ public class ManagementLogger implements MessageReader { private static final Logger log = LoggerFactory.getLogger(ManagementLogger.class); private static final Duration SLEEP_INTERVAL = Duration.ofMillis(100L); private static final Duration MAX_WAIT_TIME = Duration.ofSeconds(60L); private final StandardJanusGraph graph; private final SchemaCache schemaCache; private final Log sysLog; /** * This belongs in JanusGraphConfig. */ private final TimestampProvider times; private final AtomicInteger evictionTriggerCounter = new AtomicInteger(0); private final ConcurrentMap<Long,EvictionTrigger> evictionTriggerMap = new ConcurrentHashMap<Long,EvictionTrigger>(); public ManagementLogger(StandardJanusGraph graph, Log sysLog, SchemaCache schemaCache, TimestampProvider times) { this.graph = graph; this.schemaCache = schemaCache; this.sysLog = sysLog; this.times = times; Preconditions.checkNotNull(times); } @Override public void read(Message message) { ReadBuffer in = message.getContent().asReadBuffer(); String senderId = message.getSenderId(); Serializer serializer = graph.getDataSerializer(); MgmtLogType logType = serializer.readObjectNotNull(in, MgmtLogType.class); Preconditions.checkNotNull(logType); if (logType==MgmtLogType.CACHED_TYPE_EVICTION) { long evictionId = VariableLong.readPositive(in); long numEvictions = VariableLong.readPositive(in); for (int i = 0; i < numEvictions; i++) { long typeId = VariableLong.readPositive(in); schemaCache.expireSchemaElement(typeId); } Thread ack = new Thread(new SendAckOnTxClose(evictionId,senderId,graph.getOpenTransactions())); ack.setDaemon(true); ack.start(); } else if (logType == MgmtLogType.CACHED_TYPE_EVICTION_ACK) { String receiverId = serializer.readObjectNotNull(in,String.class); long evictionId = VariableLong.readPositive(in); if (receiverId.equals(graph.getConfiguration().getUniqueGraphId())) { //Acknowledgements targeted at this instance EvictionTrigger evictTrigger = evictionTriggerMap.get(evictionId); if (evictTrigger!=null) { evictTrigger.receivedAcknowledgement(senderId); } else log.error("Could not find eviction trigger for {} from {}",evictionId,senderId); } } else assert logType == MgmtLogType.CONFIG_MUTATION; } public void sendCacheEviction(Set<JanusGraphSchemaVertex> updatedTypes, List<Callable<Boolean>> updatedTypeTriggers, Set<String> openInstances) { Preconditions.checkArgument(!openInstances.isEmpty()); long evictionId = evictionTriggerCounter.incrementAndGet(); evictionTriggerMap.put(evictionId,new EvictionTrigger(evictionId,updatedTypeTriggers,openInstances)); DataOutput out = graph.getDataSerializer().getDataOutput(128); out.writeObjectNotNull(MgmtLogType.CACHED_TYPE_EVICTION); VariableLong.writePositive(out,evictionId); VariableLong.writePositive(out,updatedTypes.size()); for (JanusGraphSchemaVertex type : updatedTypes) { assert type.hasId(); VariableLong.writePositive(out,type.longId()); } sysLog.add(out.getStaticBuffer()); } private class EvictionTrigger { final long evictionId; final List<Callable<Boolean>> updatedTypeTriggers; final ImmutableSet<String> openInstances; final AtomicInteger ackCounter; private EvictionTrigger(long evictionId, List<Callable<Boolean>> updatedTypeTriggers, Set<String> openInstances) { this.evictionId = evictionId; this.updatedTypeTriggers = updatedTypeTriggers; this.openInstances = ImmutableSet.copyOf(openInstances); this.ackCounter = new AtomicInteger(openInstances.size()); } void receivedAcknowledgement(String senderId) { if (openInstances.contains(senderId)) { int countdown = ackCounter.decrementAndGet(); log.debug("Received acknowledgement for eviction [{}] from senderID={} ({} more acks still outstanding)", evictionId, senderId, countdown); if (countdown==0) { //Trigger actions for (Callable<Boolean> trigger : updatedTypeTriggers) { try { boolean success = trigger.call(); assert success; } catch (Throwable e) { log.error("Could not execute trigger ["+trigger.toString()+"] for eviction ["+evictionId+"]",e); } } log.info("Received all acknowledgements for eviction [{}]",evictionId); evictionTriggerMap.remove(evictionId,this); } } } } private class SendAckOnTxClose implements Runnable { private final long evictionId; private final Set<? extends JanusGraphTransaction> openTx; private final String originId; private SendAckOnTxClose(long evictionId, String originId, Set<? extends JanusGraphTransaction> openTx) { this.evictionId = evictionId; this.openTx = openTx; this.originId = originId; } @Override public void run() { // long startTime = Timestamps.MICRO.getTime(); Timer t = times.getTimer().start(); while (true) { boolean txStillOpen = false; Iterator<? extends JanusGraphTransaction> iter = openTx.iterator(); while (iter.hasNext()) { if (iter.next().isClosed()) { iter.remove(); } else { txStillOpen = true; } } if (!txStillOpen) { //Send ack and finish up DataOutput out = graph.getDataSerializer().getDataOutput(64); out.writeObjectNotNull(MgmtLogType.CACHED_TYPE_EVICTION_ACK); out.writeObjectNotNull(originId); VariableLong.writePositive(out,evictionId); try { sysLog.add(out.getStaticBuffer()); log.debug("Sent {}: evictionID={} originID={}", MgmtLogType.CACHED_TYPE_EVICTION_ACK, evictionId, originId); } catch (ResourceUnavailableException e) { //During shutdown, this event may be triggered but the log is already closed. The failure to send the acknowledgement //can then be ignored log.warn("System log has already shut down. Did not sent {}: evictionID={} originID={}",MgmtLogType.CACHED_TYPE_EVICTION_ACK,evictionId,originId); } break; } if (MAX_WAIT_TIME.compareTo(t.elapsed()) < 0) { //Break out if waited too long log.error("Evicted [{}] from cache but waiting too long for transactions to close. Stale transaction alert on: {}",getId(),openTx); break; } try { times.sleepPast(times.getTime().plus(SLEEP_INTERVAL)); } catch (InterruptedException e) { log.error("Interrupted eviction ack thread for "+getId(),e); break; } } } public String getId() { return evictionId+"@"+originId; } } }