Link Search Menu Expand Document

Buck - Artifact Cache Decorators [Java]

Status
PUBLISHED
Project
Buck
Project home page
https://github.com/facebook/buck
Language
Java
Tags
#decorator

Help Code Catalog grow: suggest your favorite code or weight in on open article proposals.

Table of contents
  1. Context
  2. Problem
  3. Overview
  4. Implementation details
  5. Testing
  6. Observations
  7. References
  8. Copyright notice

Context

Buck is a multi-language build system developed and used by Facebook.

Buck avoids rebuilding the same module twice by caching build artifacts and metadata. It employs various caching strategies, such as caching on the local disk, in SQLite database or in a shared cache over HTTP.

Caches obey the ArtifactCache interface, which defines methods such as fetchAsync or store.

Problem

Various embellishments, such as retries or even logging, need to be added to some, but not all, cache client instances.

Overview

The solution uses the classical Decorator design pattern.

The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. - Wikipedia

Buck implements 3 decorators for ArtifactCache:

  1. LoggingArtifactCacheDecorator - logs caching events to an event bus;
  2. RetryingCacheDecorator - retries failed requests;
  3. TwoLevelArtifactCacheDecorator - adds a two-level caching scheme, the details of which are not in the scope of this article.

The decorators implement the same ArtifactCache interface as the actual caches. Their constructors accept ArtifactCache delegate and some decorator-specific parameters. Most of the work is delegated to the delegate, while delegators provide additional functionality. Users of the ArtifactCache interface don’t distinguish between “actual” caches and decorators.

The decorators also implement the CacheDecorator interface with the only method ArtifactCache getDelegate(). As far as we can tell, this method is only used in testing.

Concrete instances of ArtifactCache with appropriate decorators are instanciated by ArtifactCaches factory class.

Implementation details

Let’s look at LoggingArtifactCacheDecorator. All it does is call eventBus.post() before and after fetching or storing artifacts in the underlying cache.

/**
 * Decorator for wrapping a {@link ArtifactCache} to log a {@link ArtifactCacheEvent} for the start
 * and finish of each event. The underlying cache must only provide synchronous operations.
 */
public class LoggingArtifactCacheDecorator implements ArtifactCache, CacheDecorator {
  private final BuckEventBus eventBus;
  private final ArtifactCache delegate;
  private final ArtifactCacheEventFactory eventFactory;

  public LoggingArtifactCacheDecorator(
      BuckEventBus eventBus, ArtifactCache delegate, ArtifactCacheEventFactory eventFactory) {
    this.eventBus = eventBus;
    this.delegate = delegate;
    this.eventFactory = eventFactory;
  }

  @Override
  public ListenableFuture<CacheResult> fetchAsync(
      @Nullable BuildTarget target, RuleKey ruleKey, LazyPath output) {
    ArtifactCacheEvent.Started started =
        eventFactory.newFetchStartedEvent(ImmutableSet.of(ruleKey));
    eventBus.post(started);
    CacheResult fetchResult = Futures.getUnchecked(delegate.fetchAsync(target, ruleKey, output));
    eventBus.post(eventFactory.newFetchFinishedEvent(started, fetchResult));
    return Futures.immediateFuture(fetchResult);
  }

  @Override
  public void skipPendingAndFutureAsyncFetches() {
    delegate.skipPendingAndFutureAsyncFetches();
  }

  @Override
  public ListenableFuture<Unit> store(ArtifactInfo info, BorrowablePath output) {
    ArtifactCacheEvent.Started started =
        eventFactory.newStoreStartedEvent(info.getRuleKeys(), info.getMetadata());
    eventBus.post(started);
    ListenableFuture<Unit> storeFuture = delegate.store(info, output);
    eventBus.post(eventFactory.newStoreFinishedEvent(started));
    return storeFuture;
  }

  @Override
  public ListenableFuture<ImmutableMap<RuleKey, CacheResult>> multiContainsAsync(
      ImmutableSet<RuleKey> ruleKeys) {
    ArtifactCacheEvent.Started started = eventFactory.newContainsStartedEvent(ruleKeys);
    eventBus.post(started);

    return Futures.transform(
        delegate.multiContainsAsync(ruleKeys),
        results -> {
          eventBus.post(eventFactory.newContainsFinishedEvent(started, results));
          return results;
        },
        MoreExecutors.directExecutor());
  }

  @Override
  public ListenableFuture<CacheDeleteResult> deleteAsync(List<RuleKey> ruleKeys) {
    return delegate.deleteAsync(ruleKeys);
  }

  @Override
  public CacheReadMode getCacheReadMode() {
    return delegate.getCacheReadMode();
  }

  @Override
  public void close() {
    delegate.close();
  }

  @Override
  public ArtifactCache getDelegate() {
    return delegate;
  }
}

Similarly, RetryingCacheDecorator passes everything down to the delegate, retrying failed fetches:

public class RetryingCacheDecorator implements ArtifactCache, CacheDecorator {

  private static final Logger LOG = Logger.get(RetryingCacheDecorator.class);

  private final ArtifactCache delegate;
  private final int maxFetchRetries;
  private final BuckEventBus buckEventBus;
  private final ArtifactCacheMode cacheMode;

  public RetryingCacheDecorator(
      ArtifactCacheMode cacheMode,
      ArtifactCache delegate,
      int maxFetchRetries,
      BuckEventBus buckEventBus) {
    Preconditions.checkArgument(maxFetchRetries > 0);

    this.cacheMode = cacheMode;
    this.delegate = delegate;
    this.maxFetchRetries = maxFetchRetries;
    this.buckEventBus = buckEventBus;
  }

  @Override
  public ListenableFuture<CacheResult> fetchAsync(
      @Nullable BuildTarget target, RuleKey ruleKey, LazyPath output) {
    List<String> allCacheErrors = new ArrayList<>();
    ListenableFuture<CacheResult> resultFuture = delegate.fetchAsync(target, ruleKey, output);
    for (int retryCount = 1; retryCount < maxFetchRetries; retryCount++) {
      int retryCountForLambda = retryCount;
      resultFuture =
          Futures.transformAsync(
              resultFuture,
              result -> {
                if (result.getType() != CacheResultType.ERROR) {
                  return Futures.immediateFuture(result);
                }
                result.cacheError().ifPresent(allCacheErrors::add);
                LOG.info(
                    "Failed to fetch %s after %d/%d attempts, exception: %s",
                    ruleKey, retryCountForLambda + 1, maxFetchRetries, result.cacheError());
                return delegate.fetchAsync(target, ruleKey, output);
              });
    }
    return Futures.transform(
        resultFuture,
        result -> {
          if (result.getType() != CacheResultType.ERROR) {
            return result;
          }
          String msg = String.join("\n", allCacheErrors);
          if (!msg.contains(NoHealthyServersException.class.getName())) {
            buckEventBus.post(
                ConsoleEvent.warning(
                    "Failed to fetch %s over %s after %d attempts.",
                    ruleKey, cacheMode.name(), maxFetchRetries));
          }
          return result.withCacheError(Optional.of(msg));
        },
        MoreExecutors.directExecutor());
  }

  // ...
  // Just delegates
}

TwoLevelArtifactCacheDecorator is a lot more involved, but its details are not in the scope of this article.

Testing

RetryingCacheDecorator and LoggingArtifactCacheDecorator aren’t tested directly. TwoLevelArtifactCacheDecorator has its own test.

Observations

  • If most decorator methods simply delegate to the underlying cache without doing anything else, perhaps there could be a BaseCacheDecorator that would simply delegate all calls, and concrete decorators could inherit from BaseCacheDecorator and only override those methods where they need to do something.

References

Stockfish is licensed under the Apache License 2.0.

Copyright (c) Facebook, Inc. and its affiliates.