// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.exec;

import com.google.devtools.build.lib.actions.ActionContext;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionStrategy;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionPolicy;
import com.google.devtools.build.lib.vfs.Path;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.NoSuchElementException;

/**
 * A cache that can lookup a {@link SpawnResult} given a {@link Spawn}, and can also upload the
 * results of an executed spawn to the cache.
 *
 * <p>This is an experimental interface to implement caching with sandboxed local execution.
 */
public interface SpawnCache extends ActionContext {
  /** A no-op implementation that has no result, and performs no upload. */
  public static CacheHandle NO_RESULT_NO_STORE = new CacheHandle() {
    @Override
    public boolean hasResult() {
      return false;
    }

    @Override
    public SpawnResult getResult() {
      throw new NoSuchElementException();
    }

    @Override
    public boolean willStore() {
      return false;
    }

    @Override
    public void store(SpawnResult result, Collection<Path> files)
        throws InterruptedException, IOException {
      // Do nothing.
    }

    @Override
    public void close() {
    }
  };

  /**
   * Helper method to create a {@link CacheHandle} from a successful {@link SpawnResult} instance.
   */
  public static CacheHandle success(final SpawnResult result) {
    return new CacheHandle() {
      @Override
      public boolean hasResult() {
        return true;
      }

      @Override
      public SpawnResult getResult() {
        return result;
      }

      @Override
      public boolean willStore() {
        return false;
      }

      @Override
      public void store(SpawnResult result, Collection<Path> files)
          throws InterruptedException, IOException {
        throw new IllegalStateException();
      }

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

  /** A no-op spawn cache. */
  @ExecutionStrategy(
      name = {"no-cache"},
      contextType = SpawnCache.class
  )
  public static class NoSpawnCache implements SpawnCache {
    @Override
    public CacheHandle lookup(Spawn spawn, SpawnExecutionPolicy context) {
      return SpawnCache.NO_RESULT_NO_STORE;
    }
  }

  /** A no-op implementation that has no results and performs no stores. */
  public static SpawnCache NO_CACHE = new NoSpawnCache();

  /**
   * This object represents both a successful and an unsuccessful cache lookup. If
   * {@link #hasResult} returns true, then {@link #getResult} must successfully return a non-null
   * instance (use the {@link #success} helper method). Otherwise {@link #getResult} should throw an
   * {@link IllegalStateException}.
   *
   * <p>If {@link #hasResult} returns false, then {@link #store} may upload the result to the cache
   * after successful execution.
   *
   * <p>Note that this interface extends {@link Closeable}, and callers must guarantee that
   * {@link #close} is called on this entry (e.g., by using try-with-resources) to free up any
   * acquired resources.
   */
  interface CacheHandle extends Closeable {
    /** Returns whether the cache lookup was successful. */
    boolean hasResult();

    /**
     * Returns the cached result.
     *
     * @throws NoSuchElementException if there is no result in this cache entry
     */
    SpawnResult getResult();

    /**
     * Returns true if the store call will actually do work. Use this to avoid unnecessary work
     * before store if it won't do anything.
     */
    boolean willStore();

    /**
     * Called after successful {@link Spawn} execution, which may or may not store the result in the
     * cache.
     *
     * <p>A cache may silently return from a failed store operation. We recommend to err on the side
     * of raising an exception rather than returning silently, and to offer command-line flags to
     * tweak this default policy as needed.
     *
     * <p>If the current thread is interrupted, then this method should return as quickly as
     * possible with an {@link InterruptedException}.
     */
    void store(SpawnResult result, Collection<Path> files)
        throws InterruptedException, IOException;
  }

  /**
   * Perform a spawn lookup. This method is similar to {@link SpawnRunner#exec}, taking the same
   * parameters and being allowed to throw the same exceptions. The intent for this method is to
   * compute a cache lookup key for the given spawn, looking it up in an implementation-dependent
   * cache (can be either on the local or remote machine), and returning a non-null
   * {@link CacheHandle} instance.
   *
   * <p>If the lookup was successful, this method should write the cached outputs to their
   * corresponding output locations in the output tree, as well as stdout and stderr, after
   * notifying {@link SpawnExecutionPolicy#lockOutputFiles}.
   *
   * <p>If the lookup was unsuccessful, this method can return a {@link CacheHandle} instance that
   * has no result, but uploads the results of the execution to the cache. The reason for a callback
   * object is for the cache to store expensive intermediate values (such as the cache key) that are
   * needed both for the lookup and the subsequent store operation.
   *
   * <p>The lookup must not succeed for non-cachable spawns. See {@link Spawns#mayBeCached()}.
   *
   * <p>Note that cache stores may be disabled, in which case the returned {@link CacheHandle}
   * instance's {@link CacheHandle#store} is a no-op.
   */
  CacheHandle lookup(Spawn spawn, SpawnExecutionPolicy context)
      throws ExecException, IOException, InterruptedException;
}
