/* * Copyright 2012 Netflix, Inc. * * 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 io.fathom.auto.locks; import io.fathom.auto.TimeSpan; import io.fathom.auto.config.Hostname; import java.io.IOException; import java.security.SecureRandom; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; public abstract class PseudoLockBase implements Lock { private static final Logger log = LoggerFactory.getLogger(PseudoLockBase.class); private final long timeoutMs; private final long pollingMs; private final long settlingMs; // all guarded by sync private long lastUpdateMs = 0; private boolean ownsTheLock; private String key; private long lockStartMs = 0; private boolean createdFile; private static final String content = Hostname.getHostname(); private static final Random random = new SecureRandom(); private static final String SEPARATOR = "_"; private static final TimeSpan DEFAULT_SETTLING_MS = TimeSpan.seconds(5); private static final int MISSING_KEY_FACTOR = 10; /** * @param lockPrefix * key prefix * @param timeoutMs * max age for locks * @param pollingMs * how often to poll S3 */ public PseudoLockBase(TimeSpan timeoutMs, TimeSpan pollingMs) { this(timeoutMs, pollingMs, DEFAULT_SETTLING_MS); } /** * @param lockPrefix * key prefix * @param timeoutMs * max age for locks * @param pollingMs * how often to poll S3 * @param settlingMs * how long to wait for S3 to reach consistency */ public PseudoLockBase(TimeSpan timeoutMs, TimeSpan pollingMs, TimeSpan settlingMs) { this.settlingMs = settlingMs.toMillis(); this.pollingMs = pollingMs.toMillis(); this.timeoutMs = timeoutMs.toMillis(); } /** * Acquire the lock, blocking at most <code>maxWait</code> until it is * acquired * * * @param log * the logger * @param maxWait * max time to wait * @param unit * time unit * @return true if the lock was acquired * @throws InterruptedException * @throws Exception * errors */ @Override public synchronized boolean tryLock(long maxWait, TimeUnit unit) throws InterruptedException { if (ownsTheLock) { throw new IllegalStateException("Already locked"); } log.debug("Trying to obtain lock: {}", this); lockStartMs = System.currentTimeMillis(); key = newRandomSequence(); long startMs = System.currentTimeMillis(); boolean hasMaxWait = (unit != null); long maxWaitMs = hasMaxWait ? TimeUnit.MILLISECONDS.convert(maxWait, unit) : Long.MAX_VALUE; Preconditions.checkState(maxWaitMs >= settlingMs, String.format("The maxWait ms (%d) is less than the settling ms (%d)", maxWaitMs, settlingMs)); try { createFile(key, content); createdFile = true; } catch (IOException e) { throw new RuntimeException("Error creating lock file", e); } for (;;) { try { checkUpdate(); } catch (IOException e) { throw new RuntimeException("Error checking lock file status", e); } if (ownsTheLock) { break; } long thisWaitMs; if (hasMaxWait) { long elapsedMs = System.currentTimeMillis() - startMs; thisWaitMs = maxWaitMs - elapsedMs; if (thisWaitMs <= 0) { log.error(String.format("Could not acquire lock within %d ms, polling: %d ms, key: %s", maxWaitMs, pollingMs, key)); break; } } else { thisWaitMs = pollingMs; } wait(Math.min(pollingMs, thisWaitMs)); } return ownsTheLock; } /** * Release the lock * * @throws Exception * errors */ @Override public synchronized void unlock() { if (createdFile) { try { deleteFile(key); } catch (IOException e) { throw new RuntimeException("Error releasing lock", e); } createdFile = false; } notifyAll(); ownsTheLock = false; } protected abstract void createFile(String key, String content) throws IOException; protected abstract void deleteFile(String key) throws IOException; protected abstract List<String> getFileNames() throws IOException; private synchronized void checkUpdate() throws IOException { if ((System.currentTimeMillis() - lastUpdateMs) < pollingMs) { return; } List<String> keys = getFileNames(); log.debug(String.format("keys: %s", keys)); keys = cleanOldObjects(keys); log.debug(String.format("cleaned keys: %s", keys)); Collections.sort(keys); if (keys.size() > 0) { String lockerKey = keys.get(0); long lockerAge = System.currentTimeMillis() - getEpochStampForKey(key); ownsTheLock = false; if (lockerKey.equals(key)) { if (lockerAge >= settlingMs) { ownsTheLock = true; } else { log.debug("Lock match, but waiting to settle: {} vs {}", lockerAge, settlingMs); } } } else { long elapsed = System.currentTimeMillis() - lockStartMs; if (elapsed > (settlingMs * MISSING_KEY_FACTOR)) { throw new IOException(String.format("Our key is missing. Key: %s, Elapsed: %d, Max Wait: %d", key, elapsed, settlingMs * MISSING_KEY_FACTOR)); } } lastUpdateMs = System.currentTimeMillis(); notifyAll(); } private List<String> cleanOldObjects(List<String> keys) throws IOException { List<String> newKeys = Lists.newArrayList(); for (String key : keys) { long epochStamp = getEpochStampForKey(key); if (!key.equals(this.key) && ((System.currentTimeMillis() - epochStamp) > timeoutMs)) { deleteFile(key); } else { newKeys.add(key); } } return newKeys; } private static long getEpochStampForKey(String key) { String[] parts = key.split(SEPARATOR); long millisecondStamp = 0; try { millisecondStamp = Long.parseLong(parts[0]); } catch (NumberFormatException ignore) { // ignore log.warn("Error parsing epoch stamp for key: " + key); } return millisecondStamp; } private String newRandomSequence() { return "" + System.currentTimeMillis() + SEPARATOR + Math.abs(random.nextLong()); } @Override public void lock() { throw new UnsupportedOperationException(); } @Override public void lockInterruptibly() throws InterruptedException { throw new UnsupportedOperationException(); } @Override public boolean tryLock() { throw new UnsupportedOperationException(); } @Override public Condition newCondition() { throw new UnsupportedOperationException(); } }