private static AtomicLong exportLongStat(String template, Object... args) { return Stats.exportLong(String.format(template, args)); }
@VisibleForTesting static class LogStream implements org.apache.aurora.scheduler.log.Log.Stream { @VisibleForTesting static final class OpStats { private final String opName; private final SlidingStats timing; private final AtomicLong timeouts; private final AtomicLong failures; OpStats(String opName) { this.opName = MorePreconditions.checkNotBlank(opName); timing = new SlidingStats("scheduler_log_native_" + opName, "nanos"); timeouts = exportLongStat("scheduler_log_native_%s_timeouts", opName); failures = exportLongStat("scheduler_log_native_%s_failures", opName); } private static AtomicLong exportLongStat(String template, Object... args) { return Stats.exportLong(String.format(template, args)); } } private static final Function<Log.Entry, LogEntry> MESOS_ENTRY_TO_ENTRY = new Function<Log.Entry, LogEntry>() { @Override public LogEntry apply(Log.Entry entry) { return new LogEntry(entry); } }; private final OpStats read = new OpStats("read"); private final OpStats append = new OpStats("append"); private final OpStats truncate = new OpStats("truncate"); private final AtomicLong entriesSkipped = Stats.exportLong("scheduler_log_native_native_entries_skipped"); private final LogInterface log; private final ReaderInterface reader; private final long readTimeout; private final TimeUnit readTimeUnit; private final Provider<WriterInterface> writerFactory; private final long writeTimeout; private final TimeUnit writeTimeUnit; private final byte[] noopEntry; private WriterInterface writer; LogStream( LogInterface log, ReaderInterface reader, Amount<Long, Time> readTimeout, Provider<WriterInterface> writerFactory, Amount<Long, Time> writeTimeout, byte[] noopEntry) { this.log = log; this.reader = reader; this.readTimeout = readTimeout.getValue(); this.readTimeUnit = readTimeout.getUnit().getTimeUnit(); this.writerFactory = writerFactory; this.writeTimeout = writeTimeout.getValue(); this.writeTimeUnit = writeTimeout.getUnit().getTimeUnit(); this.noopEntry = noopEntry; } @Override public Iterator<Entry> readAll() throws StreamAccessException { // TODO(John Sirois): Currently we must be the coordinator to ensure we get the 'full read' // of log entries expected by the users of the org.apache.aurora.scheduler.log.Log interface. // Switch to another method of ensuring this when it becomes available in mesos' log // interface. try { append(noopEntry); } catch (StreamAccessException e) { throw new StreamAccessException("Error writing noop prior to a read", e); } final Log.Position from = reader.beginning(); final Log.Position to = end().unwrap(); // Reading all the entries at once may cause large garbage collections. Instead, we // lazily read the entries one by one as they are requested. // TODO(Benjamin Hindman): Eventually replace this functionality with functionality // from the Mesos Log. return new UnmodifiableIterator<Entry>() { private long position = Longs.fromByteArray(from.identity()); private final long endPosition = Longs.fromByteArray(to.identity()); private Entry entry = null; @Override public boolean hasNext() { if (entry != null) { return true; } while (position <= endPosition) { long start = System.nanoTime(); try { Log.Position p = log.position(Longs.toByteArray(position)); if (LOG.isLoggable(Level.FINE)) { LOG.fine("Reading position " + position + " from the log"); } List<Log.Entry> entries = reader.read(p, p, readTimeout, readTimeUnit); // N.B. HACK! There is currently no way to "increment" a position. Until the Mesos // Log actually provides a way to "stream" the log, we approximate as much by // using longs via Log.Position.identity and Log.position. position++; // Reading positions in this way means it's possible that we get an "invalid" entry // (e.g., in the underlying log terminology this would be anything but an append) // which will be removed from the returned entries resulting in an empty list. // We skip these. if (entries.isEmpty()) { entriesSkipped.getAndIncrement(); } else { entry = MESOS_ENTRY_TO_ENTRY.apply(Iterables.getOnlyElement(entries)); return true; } } catch (TimeoutException e) { read.timeouts.getAndIncrement(); throw new StreamAccessException("Timeout reading from log.", e); } catch (Log.OperationFailedException e) { read.failures.getAndIncrement(); throw new StreamAccessException("Problem reading from log", e); } finally { read.timing.accumulate(System.nanoTime() - start); } } return false; } @Override public Entry next() { if (entry == null && !hasNext()) { throw new NoSuchElementException(); } Entry result = Preconditions.checkNotNull(entry); entry = null; return result; } }; } @Override public LogPosition append(final byte[] contents) throws StreamAccessException { Preconditions.checkNotNull(contents); Log.Position position = mutate( append, new Mutation<Log.Position>() { @Override public Log.Position apply(WriterInterface logWriter) throws TimeoutException, Log.WriterFailedException { return logWriter.append(contents, writeTimeout, writeTimeUnit); } }); return LogPosition.wrap(position); } @Timed("scheduler_log_native_truncate_before") @Override public void truncateBefore(org.apache.aurora.scheduler.log.Log.Position position) throws StreamAccessException { Preconditions.checkArgument(position instanceof LogPosition); final Log.Position before = ((LogPosition) position).unwrap(); mutate( truncate, new Mutation<Void>() { @Override public Void apply(WriterInterface logWriter) throws TimeoutException, Log.WriterFailedException { logWriter.truncate(before, writeTimeout, writeTimeUnit); return null; } }); } @VisibleForTesting interface Mutation<T> { T apply(WriterInterface writer) throws TimeoutException, Log.WriterFailedException; } @VisibleForTesting synchronized <T> T mutate(OpStats stats, Mutation<T> mutation) { long start = System.nanoTime(); if (writer == null) { writer = writerFactory.get(); } try { return mutation.apply(writer); } catch (TimeoutException e) { stats.timeouts.getAndIncrement(); throw new StreamAccessException("Timeout performing log " + stats.opName, e); } catch (Log.WriterFailedException e) { stats.failures.getAndIncrement(); // We must throw away a writer on any write failure - this could be because of a coordinator // election in which case we must trigger a new election. writer = null; throw new StreamAccessException("Problem performing log" + stats.opName, e); } finally { stats.timing.accumulate(System.nanoTime() - start); } } private LogPosition end() { return LogPosition.wrap(reader.ending()); } @Override public void close() { // noop } private static class LogPosition implements org.apache.aurora.scheduler.log.Log.Position { private final Log.Position underlying; LogPosition(Log.Position underlying) { this.underlying = underlying; } static LogPosition wrap(Log.Position position) { return new LogPosition(position); } Log.Position unwrap() { return underlying; } @Override public int compareTo(Position o) { Preconditions.checkArgument(o instanceof LogPosition); return underlying.compareTo(((LogPosition) o).underlying); } } private static class LogEntry implements org.apache.aurora.scheduler.log.Log.Entry { private final Log.Entry underlying; public LogEntry(Log.Entry entry) { this.underlying = entry; } @Override public byte[] contents() { return underlying.data; } } }