コード例 #1
0
ファイル: Status.java プロジェクト: hanjk1234/grpc-java
/**
 * Defines the status of an operation by providing a standard {@link Code} in conjunction with an
 * optional descriptive message. Instances of {@code Status} are created by starting with the
 * template for the appropriate {@link Status.Code} and supplementing it with additional
 * information: {@code Status.NOT_FOUND.withDescription("Could not find 'important_file.txt'");}
 *
 * <p>For clients, every remote call will return a status on completion. In the case of errors this
 * status may be propagated to blocking stubs as a {@link RuntimeException} or to a listener as an
 * explicit parameter.
 *
 * <p>Similarly servers can report a status by throwing {@link StatusRuntimeException} or by passing
 * the status to a callback.
 *
 * <p>Utility functions are provided to convert a status to an exception and to extract them back
 * out.
 */
@Immutable
public final class Status {

  /**
   * The set of canonical status codes. If new codes are added over time they must choose a
   * numerical value that does not collide with any previously used value.
   */
  public enum Code {
    /** The operation completed successfully. */
    OK(0),

    /** The operation was cancelled (typically by the caller). */
    CANCELLED(1),

    /**
     * Unknown error. An example of where this error may be returned is if a Status value received
     * from another address space belongs to an error-space that is not known in this address space.
     * Also errors raised by APIs that do not return enough error information may be converted to
     * this error.
     */
    UNKNOWN(2),

    /**
     * Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION.
     * INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the
     * system (e.g., a malformed file name).
     */
    INVALID_ARGUMENT(3),

    /**
     * Deadline expired before operation could complete. For operations that change the state of the
     * system, this error may be returned even if the operation has completed successfully. For
     * example, a successful response from a server could have been delayed long enough for the
     * deadline to expire.
     */
    DEADLINE_EXCEEDED(4),

    /** Some requested entity (e.g., file or directory) was not found. */
    NOT_FOUND(5),

    /** Some entity that we attempted to create (e.g., file or directory) already exists. */
    ALREADY_EXISTS(6),

    /**
     * The caller does not have permission to execute the specified operation. PERMISSION_DENIED
     * must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED
     * instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be
     * identified (use UNAUTHENTICATED instead for those errors).
     */
    PERMISSION_DENIED(7),

    /**
     * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
     * is out of space.
     */
    RESOURCE_EXHAUSTED(8),

    /**
     * Operation was rejected because the system is not in a state required for the operation's
     * execution. For example, directory to be deleted may be non-empty, an rmdir operation is
     * applied to a non-directory, etc.
     *
     * <p>A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION,
     * ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call.
     * (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a
     * read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until
     * the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory
     * is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless
     * they have first fixed up the directory by deleting files from it.
     */
    FAILED_PRECONDITION(9),

    /**
     * The operation was aborted, typically due to a concurrency issue like sequencer check
     * failures, transaction aborts, etc.
     *
     * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
     */
    ABORTED(10),

    /**
     * Operation was attempted past the valid range. E.g., seeking or reading past end of file.
     *
     * <p>Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system
     * state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to
     * read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if
     * asked to read from an offset past the current file size.
     *
     * <p>There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend
     * using OUT_OF_RANGE (the more specific error) when it applies so that callers who are
     * iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are
     * done.
     */
    OUT_OF_RANGE(11),

    /** Operation is not implemented or not supported/enabled in this service. */
    UNIMPLEMENTED(12),

    /**
     * Internal errors. Means some invariants expected by underlying system has been broken. If you
     * see one of these errors, something is very broken.
     */
    INTERNAL(13),

    /**
     * The service is currently unavailable. This is a most likely a transient condition and may be
     * corrected by retrying with a backoff.
     *
     * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
     */
    UNAVAILABLE(14),

    /** Unrecoverable data loss or corruption. */
    DATA_LOSS(15),

    /** The request does not have valid authentication credentials for the operation. */
    UNAUTHENTICATED(16);

    private final int value;
    private final String valueAscii;

    private Code(int value) {
      this.value = value;
      this.valueAscii = Integer.toString(value);
    }

    /** The numerical value of the code. */
    public int value() {
      return value;
    }

    private Status status() {
      return STATUS_LIST.get(value);
    }

    private String valueAscii() {
      return valueAscii;
    }
  }

  // Create the canonical list of Status instances indexed by their code values.
  private static final List<Status> STATUS_LIST;

  static {
    TreeMap<Integer, Status> canonicalizer = new TreeMap<Integer, Status>();
    for (Code code : Code.values()) {
      Status replaced = canonicalizer.put(code.value(), new Status(code));
      if (replaced != null) {
        throw new IllegalStateException(
            "Code value duplication between " + replaced.getCode().name() + " & " + code.name());
      }
    }
    STATUS_LIST = new ArrayList<Status>(canonicalizer.values());
  }

  // A pseudo-enum of Status instances mapped 1:1 with values in Code. This simplifies construction
  // patterns for derived instances of Status.
  /** The operation completed successfully. */
  public static final Status OK = Code.OK.status();
  /** The operation was cancelled (typically by the caller). */
  public static final Status CANCELLED = Code.CANCELLED.status();
  /** Unknown error. See {@link Code#UNKNOWN}. */
  public static final Status UNKNOWN = Code.UNKNOWN.status();
  /** Client specified an invalid argument. See {@link Code#INVALID_ARGUMENT}. */
  public static final Status INVALID_ARGUMENT = Code.INVALID_ARGUMENT.status();
  /** Deadline expired before operation could complete. See {@link Code#DEADLINE_EXCEEDED}. */
  public static final Status DEADLINE_EXCEEDED = Code.DEADLINE_EXCEEDED.status();
  /** Some requested entity (e.g., file or directory) was not found. */
  public static final Status NOT_FOUND = Code.NOT_FOUND.status();
  /** Some entity that we attempted to create (e.g., file or directory) already exists. */
  public static final Status ALREADY_EXISTS = Code.ALREADY_EXISTS.status();
  /**
   * The caller does not have permission to execute the specified operation. See {@link
   * Code#PERMISSION_DENIED}.
   */
  public static final Status PERMISSION_DENIED = Code.PERMISSION_DENIED.status();
  /** The request does not have valid authentication credentials for the operation. */
  public static final Status UNAUTHENTICATED = Code.UNAUTHENTICATED.status();
  /**
   * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
   * is out of space.
   */
  public static final Status RESOURCE_EXHAUSTED = Code.RESOURCE_EXHAUSTED.status();
  /**
   * Operation was rejected because the system is not in a state required for the operation's
   * execution. See {@link Code#FAILED_PRECONDITION}.
   */
  public static final Status FAILED_PRECONDITION = Code.FAILED_PRECONDITION.status();
  /**
   * The operation was aborted, typically due to a concurrency issue like sequencer check failures,
   * transaction aborts, etc. See {@link Code#ABORTED}.
   */
  public static final Status ABORTED = Code.ABORTED.status();
  /** Operation was attempted past the valid range. See {@link Code#OUT_OF_RANGE}. */
  public static final Status OUT_OF_RANGE = Code.OUT_OF_RANGE.status();
  /** Operation is not implemented or not supported/enabled in this service. */
  public static final Status UNIMPLEMENTED = Code.UNIMPLEMENTED.status();
  /** Internal errors. See {@link Code#INTERNAL}. */
  public static final Status INTERNAL = Code.INTERNAL.status();
  /** The service is currently unavailable. See {@link Code#UNAVAILABLE}. */
  public static final Status UNAVAILABLE = Code.UNAVAILABLE.status();
  /** Unrecoverable data loss or corruption. */
  public static final Status DATA_LOSS = Code.DATA_LOSS.status();

  /** Return a {@link Status} given a canonical error {@link Code} value. */
  public static Status fromCodeValue(int codeValue) {
    if (codeValue < 0 || codeValue > STATUS_LIST.size()) {
      return UNKNOWN.withDescription("Unknown code " + codeValue);
    } else {
      return STATUS_LIST.get(codeValue);
    }
  }

  /** Key to bind status code to trailing metadata. */
  @Internal
  public static final Metadata.Key<Status> CODE_KEY =
      Metadata.Key.of("grpc-status", new StatusCodeMarshaller());

  /** Key to bind status message to trailing metadata. */
  @Internal
  public static final Metadata.Key<String> MESSAGE_KEY =
      Metadata.Key.of("grpc-message", Metadata.ASCII_STRING_MARSHALLER);

  /** Extract an error {@link Status} from the causal chain of a {@link Throwable}. */
  public static Status fromThrowable(Throwable t) {
    for (Throwable cause : Throwables.getCausalChain(t)) {
      if (cause instanceof StatusException) {
        return ((StatusException) cause).getStatus();
      } else if (cause instanceof StatusRuntimeException) {
        return ((StatusRuntimeException) cause).getStatus();
      }
    }
    // Couldn't find a cause with a Status
    return UNKNOWN.withCause(t);
  }

  static String formatThrowableMessage(Status status) {
    if (status.description == null) {
      return status.code.toString();
    } else {
      return status.code + ": " + status.description;
    }
  }

  private final Code code;
  private final String description;
  private final Throwable cause;

  private Status(Code code) {
    this(code, null, null);
  }

  private Status(Code code, @Nullable String description, @Nullable Throwable cause) {
    this.code = Preconditions.checkNotNull(code);
    this.description = description;
    this.cause = cause;
  }

  /** Create a derived instance of {@link Status} with the given cause. */
  public Status withCause(Throwable cause) {
    if (Objects.equal(this.cause, cause)) {
      return this;
    }
    return new Status(this.code, this.description, cause);
  }

  /** Create a derived instance of {@link Status} with the given description. */
  public Status withDescription(String description) {
    if (Objects.equal(this.description, description)) {
      return this;
    }
    return new Status(this.code, description, this.cause);
  }

  /**
   * Create a derived instance of {@link Status} augmenting the current description with additional
   * detail.
   */
  public Status augmentDescription(String additionalDetail) {
    if (additionalDetail == null) {
      return this;
    } else if (this.description == null) {
      return new Status(this.code, additionalDetail, this.cause);
    } else {
      return new Status(this.code, this.description + "\n" + additionalDetail, this.cause);
    }
  }

  /** The canonical status code. */
  public Code getCode() {
    return code;
  }

  /** A description of this status for human consumption. */
  @Nullable
  public String getDescription() {
    return description;
  }

  /** The underlying cause of an error. */
  @Nullable
  public Throwable getCause() {
    return cause;
  }

  /** Is this status OK, i.e., not an error. */
  public boolean isOk() {
    return Code.OK == code;
  }

  /**
   * Convert this {@link Status} to a {@link RuntimeException}. Use {@link #fromThrowable} to
   * recover this {@link Status} instance when the returned exception is in the causal chain.
   */
  public StatusRuntimeException asRuntimeException() {
    return new StatusRuntimeException(this);
  }

  /**
   * Convert this {@link Status} to an {@link Exception}. Use {@link #fromThrowable} to recover this
   * {@link Status} instance when the returned exception is in the causal chain.
   */
  public StatusException asException() {
    return new StatusException(this);
  }

  /** A string representation of the status useful for debugging. */
  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .add("code", code.name())
        .add("description", description)
        .add("cause", cause)
        .toString();
  }

  private static class StatusCodeMarshaller implements Metadata.AsciiMarshaller<Status> {
    @Override
    public String toAsciiString(Status status) {
      return status.getCode().valueAscii();
    }

    @Override
    public Status parseAsciiString(String serialized) {
      return fromCodeValue(Integer.valueOf(serialized));
    }
  }

  /**
   * Equality on Statuses is not well defined. Instead, do comparison based on their Code with
   * {@link #getCode}. The description and cause of the Status are unlikely to be stable, and
   * additional fields may be added to Status in the future.
   */
  @Override
  public boolean equals(Object obj) {
    return super.equals(obj);
  }

  /**
   * Hash codes on Statuses are not well defined.
   *
   * @see #equals
   */
  @Override
  public int hashCode() {
    return super.hashCode();
  }
}
コード例 #2
0
ファイル: ChannelImpl.java プロジェクト: jervisfm/grpc-java
/** A communication channel for making outgoing RPCs. */
@ThreadSafe
public final class ChannelImpl extends Channel {
  private static final Logger log = Logger.getLogger(ChannelImpl.class.getName());
  private final ClientTransportFactory transportFactory;
  private final ExecutorService executor;
  private final String userAgent;
  private final Object lock = new Object();

  /** Executor that runs deadline timers for requests. */
  private ScheduledExecutorService scheduledExecutor;

  // TODO(carl-mastrangelo): Allow clients to pass this in
  private final BackoffPolicy.Provider backoffPolicyProvider =
      new ExponentialBackoffPolicy.Provider();
  /**
   * We delegate to this channel, so that we can have interceptors as necessary. If there aren't any
   * interceptors this will just be {@link RealChannel}.
   */
  private final Channel interceptorChannel;
  /**
   * All transports that are not stopped. At the very least {@link #activeTransport} will be
   * present, but previously used transports that still have streams or are stopping may also be
   * present.
   */
  @GuardedBy("lock")
  private Collection<ClientTransport> transports = new ArrayList<ClientTransport>();
  /**
   * The transport for new outgoing requests. 'this' lock must be held when assigning to
   * activeTransport.
   */
  private volatile ClientTransport activeTransport;

  @GuardedBy("lock")
  private boolean shutdown;

  @GuardedBy("lock")
  private boolean terminated;

  private Runnable terminationRunnable;

  private long reconnectTimeMillis;
  private BackoffPolicy reconnectPolicy;

  private final ClientTransportProvider transportProvider =
      new ClientTransportProvider() {
        @Override
        public ClientTransport get() {
          return obtainActiveTransport();
        }
      };

  ChannelImpl(
      ClientTransportFactory transportFactory,
      ExecutorService executor,
      @Nullable String userAgent,
      List<ClientInterceptor> interceptors) {
    this.transportFactory = transportFactory;
    this.executor = executor;
    this.userAgent = userAgent;
    this.interceptorChannel = ClientInterceptors.intercept(new RealChannel(), interceptors);
    scheduledExecutor = SharedResourceHolder.get(TIMER_SERVICE);
  }

  /** Hack to allow executors to auto-shutdown. Not for general use. */
  // TODO(ejona86): Replace with a real API.
  void setTerminationRunnable(Runnable runnable) {
    this.terminationRunnable = runnable;
  }

  /**
   * Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately
   * cancelled.
   */
  public ChannelImpl shutdown() {
    ClientTransport savedActiveTransport;
    synchronized (lock) {
      if (shutdown) {
        return this;
      }
      shutdown = true;
      // After shutdown there are no new calls, so no new cancellation tasks are needed
      scheduledExecutor = SharedResourceHolder.release(TIMER_SERVICE, scheduledExecutor);
      savedActiveTransport = activeTransport;
      if (savedActiveTransport != null) {
        activeTransport = null;
      } else if (transports.isEmpty()) {
        terminated = true;
        lock.notifyAll();
        if (terminationRunnable != null) {
          terminationRunnable.run();
        }
      }
    }
    if (savedActiveTransport != null) {
      savedActiveTransport.shutdown();
    }
    return this;
  }

  /**
   * Initiates a forceful shutdown in which preexisting and new calls are cancelled. Although
   * forceful, the shutdown process is still not instantaneous; {@link #isTerminated()} will likely
   * return {@code false} immediately after this method returns.
   *
   * <p>NOT YET IMPLEMENTED. This method currently behaves identically to shutdown().
   */
  // TODO(ejona86): cancel preexisting calls.
  public ChannelImpl shutdownNow() {
    shutdown();
    return this;
  }

  /**
   * Returns whether the channel is shutdown. Shutdown channels immediately cancel any new calls,
   * but may still have some calls being processed.
   *
   * @see #shutdown()
   * @see #isTerminated()
   */
  public boolean isShutdown() {
    synchronized (lock) {
      return shutdown;
    }
  }

  /**
   * Waits for the channel to become terminated, giving up if the timeout is reached.
   *
   * @return whether the channel is terminated, as would be done by {@link #isTerminated()}.
   */
  public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    synchronized (lock) {
      long timeoutNanos = unit.toNanos(timeout);
      long endTimeNanos = System.nanoTime() + timeoutNanos;
      while (!terminated && (timeoutNanos = endTimeNanos - System.nanoTime()) > 0) {
        TimeUnit.NANOSECONDS.timedWait(lock, timeoutNanos);
      }
      return terminated;
    }
  }

  /**
   * Returns whether the channel is terminated. Terminated channels have no running calls and
   * relevant resources released (like TCP connections).
   *
   * @see #isShutdown()
   */
  public boolean isTerminated() {
    synchronized (lock) {
      return terminated;
    }
  }

  /**
   * Pings the remote endpoint to verify that the transport is still active. When an acknowledgement
   * is received, the given callback will be invoked using the given executor.
   *
   * <p>If the underlying transport has no mechanism by when to send a ping, this method may throw
   * an {@link UnsupportedOperationException}. The operation may {@linkplain
   * PingCallback#pingFailed(Throwable) fail} due to transient transport errors. In that case,
   * trying again may succeed.
   *
   * @see ClientTransport#ping(PingCallback, Executor)
   */
  public void ping(final PingCallback callback, final Executor executor) {
    try {
      obtainActiveTransport().ping(callback, executor);
    } catch (final RuntimeException ex) {
      executor.execute(
          new Runnable() {
            @Override
            public void run() {
              callback.pingFailed(ex);
            }
          });
    }
  }

  /*
   * Creates a new outgoing call on the channel.
   */
  @Override
  public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
      MethodDescriptor<ReqT, RespT> method, CallOptions callOptions) {
    return interceptorChannel.newCall(method, callOptions);
  }

  private ClientTransport obtainActiveTransport() {
    ClientTransport savedActiveTransport = activeTransport;
    // If we know there is an active transport and we are not in backoff mode, return quickly.
    if (savedActiveTransport != null && !(savedActiveTransport instanceof InactiveTransport)) {
      return savedActiveTransport;
    }
    synchronized (lock) {
      if (shutdown) {
        return null;
      }
      savedActiveTransport = activeTransport;
      if (savedActiveTransport instanceof InactiveTransport) {
        if (System.nanoTime() > TimeUnit.MILLISECONDS.toNanos(reconnectTimeMillis)) {
          // The timeout expired, clear the inactive transport and update the shutdown status to
          // something that is retryable.
          activeTransport = null;
          savedActiveTransport = activeTransport;
        } else {
          // We are still in backoff mode, just return the inactive transport.
          return savedActiveTransport;
        }
      }

      if (savedActiveTransport != null) {
        return savedActiveTransport;
      }
      // There is no active transport, or we just finished backoff.  Create a new transport.
      ClientTransport newActiveTransport = transportFactory.newClientTransport();
      transports.add(newActiveTransport);
      boolean failed = true;
      try {
        newActiveTransport.start(new TransportListener(newActiveTransport));
        failed = false;
      } finally {
        if (failed) {
          transports.remove(newActiveTransport);
        }
      }
      // It's possible that start() called transportShutdown() and transportTerminated(). If so, we
      // wouldn't want to make it the active transport.
      if (transports.contains(newActiveTransport)) {
        // start() must return before we set activeTransport, since activeTransport is accessed
        // without a lock.
        activeTransport = newActiveTransport;
      }
      return newActiveTransport;
    }
  }

  private class RealChannel extends Channel {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> newCall(
        MethodDescriptor<ReqT, RespT> method, CallOptions callOptions) {
      return new ClientCallImpl<ReqT, RespT>(
              method,
              new SerializingExecutor(executor),
              callOptions,
              transportProvider,
              scheduledExecutor)
          .setUserAgent(userAgent);
    }
  }

  private class TransportListener implements ClientTransport.Listener {
    private final ClientTransport transport;

    public TransportListener(ClientTransport transport) {
      this.transport = transport;
    }

    @Override
    public void transportReady() {
      synchronized (lock) {
        if (activeTransport == transport) {
          reconnectPolicy = null;
        }
      }
    }

    @Override
    public void transportShutdown(Status s) {
      synchronized (lock) {
        if (activeTransport == transport) {
          activeTransport = null;
          // This transport listener was attached to the active transport.
          if (s.isOk()) {
            return;
          }
          // Alright, something bad has happened.
          if (reconnectPolicy == null) {
            // This happens the first time something bad has happened.
            reconnectPolicy = backoffPolicyProvider.get();
            reconnectTimeMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
          }
          activeTransport = new InactiveTransport(s);
          reconnectTimeMillis += reconnectPolicy.nextBackoffMillis();
        }
      }
    }

    @Override
    public void transportTerminated() {
      synchronized (lock) {
        if (activeTransport == transport) {
          log.warning("transportTerminated called without previous transportShutdown");
          activeTransport = null;
        }
        // TODO(notcarl): replace this with something more meaningful
        transportShutdown(Status.UNKNOWN.withDescription("transport shutdown for unknown reason"));
        transports.remove(transport);
        if (shutdown && transports.isEmpty()) {
          if (terminated) {
            log.warning("transportTerminated called after already terminated");
          }
          terminated = true;
          lock.notifyAll();
          if (terminationRunnable != null) {
            terminationRunnable.run();
          }
        }
      }
    }
  }

  /** Intended for internal use only. */
  // TODO(johnbcoughlin) make this package private when we can do so with the tests.
  @VisibleForTesting
  public static final Metadata.Key<Long> TIMEOUT_KEY =
      Metadata.Key.of(HttpUtil.TIMEOUT, new TimeoutMarshaller());

  /**
   * Marshals a microseconds representation of the timeout to and from a string representation,
   * consisting of an ASCII decimal representation of a number with at most 8 digits, followed by a
   * unit: u = microseconds m = milliseconds S = seconds M = minutes H = hours
   *
   * <p>The representation is greedy with respect to precision. That is, 2 seconds will be
   * represented as `2000000u`.
   *
   * <p>See <a href="https://github.com/grpc/grpc-common/blob/master/PROTOCOL-HTTP2.md#requests">the
   * request header definition</a>
   */
  @VisibleForTesting
  static class TimeoutMarshaller implements Metadata.AsciiMarshaller<Long> {
    @Override
    public String toAsciiString(Long timeoutMicros) {
      Preconditions.checkArgument(timeoutMicros >= 0, "Negative timeout");
      long timeout;
      String timeoutUnit;
      // the smallest integer with 9 digits
      int cutoff = 100000000;
      if (timeoutMicros < cutoff) {
        timeout = timeoutMicros;
        timeoutUnit = "u";
      } else if (timeoutMicros / 1000 < cutoff) {
        timeout = timeoutMicros / 1000;
        timeoutUnit = "m";
      } else if (timeoutMicros / (1000 * 1000) < cutoff) {
        timeout = timeoutMicros / (1000 * 1000);
        timeoutUnit = "S";
      } else if (timeoutMicros / (60 * 1000 * 1000) < cutoff) {
        timeout = timeoutMicros / (60 * 1000 * 1000);
        timeoutUnit = "M";
      } else if (timeoutMicros / (60L * 60L * 1000L * 1000L) < cutoff) {
        timeout = timeoutMicros / (60L * 60L * 1000L * 1000L);
        timeoutUnit = "H";
      } else {
        throw new IllegalArgumentException("Timeout too large");
      }
      return Long.toString(timeout) + timeoutUnit;
    }

    @Override
    public Long parseAsciiString(String serialized) {
      String valuePart = serialized.substring(0, serialized.length() - 1);
      char unit = serialized.charAt(serialized.length() - 1);
      long factor;
      switch (unit) {
        case 'u':
          factor = 1;
          break;
        case 'm':
          factor = 1000L;
          break;
        case 'S':
          factor = 1000L * 1000L;
          break;
        case 'M':
          factor = 60L * 1000L * 1000L;
          break;
        case 'H':
          factor = 60L * 60L * 1000L * 1000L;
          break;
        default:
          throw new IllegalArgumentException(String.format("Invalid timeout unit: %s", unit));
      }
      return Long.parseLong(valuePart) * factor;
    }
  }

  static final SharedResourceHolder.Resource<ScheduledExecutorService> TIMER_SERVICE =
      new SharedResourceHolder.Resource<ScheduledExecutorService>() {
        @Override
        public ScheduledExecutorService create() {
          return Executors.newSingleThreadScheduledExecutor(
              new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                  Thread thread = new Thread(r);
                  thread.setDaemon(true);
                  return thread;
                }
              });
        }

        @Override
        public void close(ScheduledExecutorService instance) {
          instance.shutdown();
        }
      };

  private static final class InactiveTransport implements ClientTransport {
    private final Status shutdownStatus;

    private InactiveTransport(Status s) {
      shutdownStatus = s;
    }

    @Override
    public ClientStream newStream(
        MethodDescriptor<?, ?> method, Headers headers, ClientStreamListener listener) {
      listener.closed(shutdownStatus, new Metadata.Trailers());
      return new ClientCallImpl.NoopClientStream();
    }

    @Override
    public void start(Listener listener) {
      throw new IllegalStateException();
    }

    @Override
    public void ping(final PingCallback callback, Executor executor) {
      executor.execute(
          new Runnable() {
            @Override
            public void run() {
              callback.pingFailed(shutdownStatus.asException());
            }
          });
    }

    @Override
    public void shutdown() {
      // no-op
    }
  }
}