/* * 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 com.addthis.hydra.query; import javax.annotation.concurrent.GuardedBy; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; import com.addthis.basis.util.Parameter; import com.addthis.meshy.ChannelMaster; import com.addthis.meshy.service.file.FileReference; import com.addthis.meshy.service.file.FileSource; import com.google.common.base.Joiner; import com.google.common.collect.ConcurrentHashMultiset; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import io.netty.util.internal.PlatformDependent; /** * Supports multi-producer (server-to-server) file ref collection in a lock-free manner. This class is only * thread-safe for the meshy-facing components though, and should not be concurrently accessed from user code. * * It uses a lock-free multi-producer/ single-consumer queue implementation from netty with some extra logic to * support blocking/timed polls (the netty version only implements {@link Queue} and not {@link BlockingQueue}). * The implementation of the blocking strives to avoid random sleeping or spin-blocking by using a {@link StampedLock} * as a sort of binary semaphore. This gets us the park/unpark semantics we want without having to either directly * implement that logic or throw away the spin-then-park loop optimizations that doug lee et al already benchmarked. * * There may be a "better" way to accomplish this than to use a {@link StampedLock}, but its code seems to suggest * that it does a pretty efficient job at handling the methods we expect to call at their respective frequencies. */ class FileRefSource extends FileSource { private static final long MAX_WAIT_SECONDS = Parameter.intValue("qmaster.maxListFilesTime", 60); private static final long MAX_WAIT_NANOS = TimeUnit.SECONDS.toNanos(MAX_WAIT_SECONDS); private static final FileReference END_SIGNAL = new FileReference("END_SIGNAL", 0, 0); /** Safe for multiple producers, but only a single consumer (lock-free). Borrowed from netty. */ private final Queue<FileReference> references; /** Only used to park the consumer thread while waiting for more references (lock-free). */ private final StampedLock semaphore; private volatile boolean shortCircuited; // uses shortCircuited to ensure visibility after lazy instantiation, but otherwise is thread-safe on its own @GuardedBy("shortCircuited") private ConcurrentHashMultiset<String> lateFileRefFinds; FileRefSource(ChannelMaster master) { super(master); this.references = PlatformDependent.newMpscQueue(); this.semaphore = new StampedLock(); // throw away a writeLock so that we can immediately park the consumer this.semaphore.writeLock(); this.shortCircuited = false; } /** Called for every received file reference. Note! This method may be called concurrently by many threads. */ @Override public void receiveReference(FileReference ref) { if (!shortCircuited) { references.add(ref); semaphore.tryUnlockWrite(); } else { lateFileRefFinds.add(ref.getHostUUID()); log.debug("throwing away ref due to short circuit {}", ref); } } /** Called once after every call to receiveReference has finished. This method is called by one of many threads. */ @Override public void receiveComplete() { references.add(END_SIGNAL); semaphore.tryUnlockWrite(); log.debug("query file ref source - receive complete for {}", this); if (shortCircuited) { log.warn("Late File Finds for {}:\n{}", this, Joiner.on('\n').join(lateFileRefFinds.entrySet())); } } public SetMultimap<Integer, FileReference> getWithShortCircuit() throws InterruptedException { SetMultimap<Integer, FileReference> fileRefMap = HashMultimap.create(); long end = System.nanoTime() + MAX_WAIT_NANOS; long remaining = MAX_WAIT_NANOS; while (true) { if (semaphore.tryWriteLock(remaining, TimeUnit.NANOSECONDS) == 0) { lateFileRefFinds = ConcurrentHashMultiset.create(); shortCircuited = true; log.warn("Timed out waiting for mesh file list: {}. After waiting {}", this, MAX_WAIT_NANOS); return fileRefMap; } FileReference fileReference = references.poll(); while (fileReference != null) { if (fileReference == END_SIGNAL) { log.debug("Finished collecting file refs for: {}", this); return fileRefMap; } fileRefMap.put(getTaskId(fileReference), fileReference); fileReference = references.poll(); } remaining = end - System.nanoTime(); } } private static int getTaskId(FileReference fileReference) { String[] tokens = fileReference.name.split("/"); return Integer.parseInt(tokens[3]); } }