{"action":"created","comment":{"url":"https://api.github.com/repos/linkedin/ambry/pulls/comments/63651479","id":63651479,"diff_hunk":"@@ -0,0 +1,777 @@\n+/**\n+ * Copyright 2016 LinkedIn Corp. All rights reserved.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ * http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ */\n+package com.github.ambry.router;\n+\n+import com.github.ambry.clustermap.ClusterMap;\n+import com.github.ambry.clustermap.ReplicaId;\n+import com.github.ambry.commons.BlobId;\n+import com.github.ambry.commons.BlobIdFactory;\n+import com.github.ambry.commons.ResponseHandler;\n+import com.github.ambry.commons.ServerErrorCode;\n+import com.github.ambry.config.RouterConfig;\n+import com.github.ambry.messageformat.BlobData;\n+import com.github.ambry.messageformat.BlobType;\n+import com.github.ambry.messageformat.MessageFormatException;\n+import com.github.ambry.messageformat.MessageFormatFlags;\n+import com.github.ambry.messageformat.MessageFormatRecord;\n+import com.github.ambry.messageformat.MetadataContentSerDe;\n+import com.github.ambry.network.Port;\n+import com.github.ambry.network.RequestInfo;\n+import com.github.ambry.network.ResponseInfo;\n+import com.github.ambry.protocol.GetRequest;\n+import com.github.ambry.protocol.GetResponse;\n+import com.github.ambry.protocol.RequestOrResponse;\n+import com.github.ambry.store.StoreKey;\n+import com.github.ambry.utils.ByteBufferInputStream;\n+import com.github.ambry.utils.Time;\n+import com.github.ambry.utils.Utils;\n+import java.io.DataInputStream;\n+import java.io.IOException;\n+import java.io.InputStream;\n+import java.nio.ByteBuffer;\n+import java.util.HashMap;\n+import java.util.Iterator;\n+import java.util.List;\n+import java.util.ListIterator;\n+import java.util.Map;\n+import java.util.TreeMap;\n+import java.util.concurrent.Future;\n+import java.util.concurrent.atomic.AtomicBoolean;\n+import java.util.concurrent.atomic.AtomicInteger;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+\n+/**\n+ * GetBlobOperation class is responsible for maintaining the state associated with a GetBlob operation, and completing\n+ * it. It would help to look at this class in conjunction with {@link PutOperation}.\n+ *\n+ * Blobs that need to be fetched are either simple (those that consist of only a single chunk) or composite (those\n+ * that consists of one metadata chunk and two or more data chunks). Whether a blob is simple or composite can only be\n+ * determined after the first chunk is fetched, based on whether the first chunk turns out to be a metadata chunk or\n+ * a data chunk.\n+ *\n+ * As soon as the first data chunk is fetched (which could be the first chunk in the case of simple blobs and the\n+ * second chunk fetched in the case of composite blobs), the operation callback is invoked (and the future is marked\n+ * as done) so that the caller can start reading in data. The rest of the chunks are asynchronously fetched and\n+ * buffered up to the maximum that can be buffered. When fetched chunks are consumed by the caller, subsequent chunks\n+ * become eligible to be fetched.\n+ *\n+ * This class implements {@link ReadableStreamChannel} and passes itself as the result of this operation.\n+ */\n+class GetBlobOperation extends GetOperation<ReadableStreamChannel> implements ReadableStreamChannel {\n+  // the callback to use to complete the operation.\n+  private final OperationCompleteCallback operationCompleteCallback;\n+  // whether the operationCompleteCallback has been called already.\n+  private final AtomicBoolean operationCallbackInvoked = new AtomicBoolean(false);\n+  // The first chunk may be a metadata chunk if the blob is composite, or the only data chunk if the blob is simple.\n+  private final FirstGetChunk firstChunk;\n+  // Associated with all data chunks in the case of composite blobs. Only a fixed number of these are initialized.\n+  // Each of these is initialized with the information required to fetch a data chunk and is responsible for\n+  // retrieving and adding it to the list of chunk buffers. Once complete, they are reused to fetch subsequent data\n+  // chunks.\n+  private GetChunk[] dataChunks;\n+  // the factory to use to deserialize keys in a metadata chunk.\n+  private final BlobIdFactory blobIdFactory;\n+  // the total number of data chunks associated with this blob.\n+  private int numChunksTotal;\n+  // the total number of data chunks retrieved so far (and may or may not have been written out yet).\n+  private int numChunksRetrieved;\n+  // a list iterator to the chunk ids that need to be fetched for this operation, if this is a composite blob.\n+  private ListIterator<StoreKey> chunkIdIterator;\n+  // chunk index to retrieved chunk buffer mapping.\n+  private Map<Integer, ByteBuffer> chunkIndexToBuffer;\n+  // To find the GetChunk to hand over the response quickly.\n+  private final Map<Integer, GetChunk> correlationIdToGetChunk = new HashMap<>();\n+\n+  // ReadableStreamChannel implementation related variables:\n+\n+  // whether this ReadableStreamChannel is open.\n+  private boolean isOpen = true;\n+  // whether readInto() has been called yet by the caller on this ReadableStreamChannel.\n+  private volatile boolean readCalled = false;\n+  // The channel to write chunks of the blob into. This will be initialized when the caller calls the readInto().\n+  private AsyncWritableChannel asyncWritableChannel;\n+  // the callback to call when all the chunks are successfully written out into the asyncWritableChannel.\n+  private Callback<Long> readIntoCallback;\n+  // the future to mark as done when all the chunks are successfully written out into the asyncWritableChannel.\n+  private FutureResult<Long> readIntoFuture;\n+  // the number of bytes written out to the asyncWritableChannel. This would be the size of the blob eventually.\n+  private Long bytesWritten = 0L;\n+  // the number of chunks that have been written out to the asyncWritableChannel.\n+  private final AtomicInteger numChunksWrittenOut = new AtomicInteger(0);\n+  // the index of the next chunk that is to be written out to the asyncWritableChannel.\n+  private int indexOfNextChunkToWriteOut = 0;\n+  // whether this object has called the readIntoCallback yet.\n+  private AtomicBoolean readIntoCallbackCalled = new AtomicBoolean(false);\n+  // the callback that is passed into the asyncWritableChannel write() operation.\n+  Callback<Long> chunkAsyncWriteCallback = new Callback<Long>() {\n+    @Override\n+    public void onCompletion(Long result, Exception exception) {\n+      bytesWritten += result;\n+      if (exception != null) {\n+        operationException.set(exception);\n+      }\n+      numChunksWrittenOut.incrementAndGet();\n+    }\n+  };\n+\n+  private static final Logger logger = LoggerFactory.getLogger(GetBlobOperation.class);\n+\n+  /**\n+   * Construct a GetBlobOperation\n+   * @param routerConfig the {@link RouterConfig} containing the configs for get operations.\n+   * @param clusterMap the {@link ClusterMap} of the cluster\n+   * @param responseHandler the {@link ResponseHandler} responsible for failure detection.\n+   * @param blobIdStr the blob id associated with the operation in string form.\n+   * @param futureResult the future that will contain the result of the operation.\n+   * @param callback the callback that is to be called when the operation completes.\n+   * @param operationCompleteCallback the {@link OperationCompleteCallback} to use to complete operations.\n+   * @param time the Time instance to use.\n+   * @throws RouterException if there is an error with any of the parameters, such as an invalid blob id.\n+   */\n+  GetBlobOperation(RouterConfig routerConfig, ClusterMap clusterMap, ResponseHandler responseHandler, String blobIdStr,\n+      FutureResult<ReadableStreamChannel> futureResult, Callback<ReadableStreamChannel> callback,\n+      OperationCompleteCallback operationCompleteCallback, Time time)\n+      throws RouterException {\n+    super(routerConfig, clusterMap, responseHandler, blobIdStr, futureResult, callback, time);\n+    this.operationCompleteCallback = operationCompleteCallback;\n+    this.blobIdFactory = new BlobIdFactory(clusterMap);\n+    firstChunk = new FirstGetChunk(blobId);\n+  }\n+\n+  /**\n+   * {@inheritDoc}\n+   * <br>\n+   * If the callback for the operation was already called, then abort the reads from the channel and the caller\n+   * will be notified as part of the read callback eventually. If not, the notification will happen as part of the\n+   * operation completion callback.\n+   */\n+  @Override\n+  void abort(Exception abortCause) {\n+    if (operationCallbackInvoked.compareAndSet(false, true)) {\n+      operationCompleteCallback.completeOperation(operationFuture, operationCallback, null, abortCause);\n+    } else {\n+      operationException.set(abortCause);\n+      if (readCalled) {\n+        completeRead();\n+      }\n+    }\n+    operationCompleted = true;\n+  }\n+\n+  /**\n+   * Return the {@link MessageFormatFlags} to associate with a getBlob operation.\n+   * @return {@link MessageFormatFlags#Blob}\n+   */\n+  @Override\n+  MessageFormatFlags getOperationFlag() {\n+    return MessageFormatFlags.Blob;\n+  }\n+\n+  /**\n+   * Do all that needs to be done (cleanup, notification, etc.) on chunk completion and mark the state of the chunk\n+   * appropriately.\n+   * @param chunk the chunk that has completed.\n+   */\n+  private void onChunkOperationComplete(GetChunk chunk) {\n+    if (chunk.getChunkException() != null) {\n+      // if operation callback was already called, then this exception will have to be notified as part of the\n+      // read callback.\n+      setOperationException(chunk.getChunkException());\n+    }\n+    if (chunk == firstChunk) {\n+      if (operationCallbackInvoked.compareAndSet(false, true)) {\n+        // Complete the operation from the caller's perspective, so that the caller can start reading from the\n+        // channel. The operation will not be marked as complete internally as subsequent chunk retrievals and\n+        // channel writes will need to happen and for that, this operation needs the GetManager to poll it\n+        // periodically. If any exception is encountered while processing subsequent chunks, those will be notified\n+        // during the channel read.\n+        ReadableStreamChannel result = getOperationException() == null ? this : null;\n+        operationCompleteCallback\n+            .completeOperation(operationFuture, operationCallback, result, getOperationException());\n+      }\n+    }\n+    chunk.postCompletionCleanup();\n+    maybeWriteToChannel();\n+  }\n+\n+  /**\n+   * Handle the given {@link ResponseInfo} by handing it over to the appropriate chunk that issued the request.\n+   * @param responseInfo the {@link ResponseInfo} to be handled.\n+   */\n+  @Override\n+  void handleResponse(ResponseInfo responseInfo) {\n+    GetChunk getChunk =\n+        correlationIdToGetChunk.remove(((RequestOrResponse) responseInfo.getRequest()).getCorrelationId());\n+    getChunk.handleResponse(responseInfo);\n+    if (getChunk.isComplete()) {\n+      onChunkOperationComplete(getChunk);\n+    }\n+  }\n+\n+  /**\n+   * Attempt to write the data associated with the blob to the channel passed in by the caller (if the caller has\n+   * done so).\n+   */\n+  private void maybeWriteToChannel() {\n+    // if there are chunks available to be written out, do now.\n+    if (firstChunk.isComplete() && readCalled) {\n+      while (indexOfNextChunkToWriteOut < numChunksTotal && chunkIndexToBuffer\n+          .containsKey(indexOfNextChunkToWriteOut)) {\n+        ByteBuffer chunkBuf = chunkIndexToBuffer.remove(indexOfNextChunkToWriteOut);\n+        asyncWritableChannel.write(chunkBuf, chunkAsyncWriteCallback);\n+        indexOfNextChunkToWriteOut++;\n+      }\n+      if (operationException.get() != null || numChunksWrittenOut.get() == numChunksTotal) {\n+        completeRead();\n+        operationCompleted = true;\n+      }\n+    }\n+  }\n+\n+  /**\n+   * Complete the read from this {@link ReadableStreamChannel} by invoking the callback and marking the future.\n+   */\n+  void completeRead() {\n+    if (readIntoCallbackCalled.compareAndSet(false, true)) {\n+      if (operationException.get() != null) {\n+        bytesWritten = null;\n+      }\n+      readIntoFuture.done(bytesWritten, operationException.get());\n+      if (readIntoCallback != null) {\n+        readIntoCallback.onCompletion(bytesWritten, operationException.get());\n+      }\n+    }\n+  }\n+\n+  /**\n+   * As part of the poll, GetBlobOperation fetches new requests to be issued and updates the state based on\n+   * whether previously issued requests have timed out and so on. Additionally, any writes of previously retrieved\n+   * chunk buffers into the {@link AsyncWritableChannel} passed in by the caller also happens within this method.\n+   * @param requestRegistrationCallback the {@link RequestRegistrationCallback} to call for every request that gets\n+   *                                    created as part of this poll operation.\n+   */\n+  @Override\n+  void poll(RequestRegistrationCallback<GetOperation> requestRegistrationCallback) {\n+    if (operationCompleted) {\n+      return;\n+    }\n+    if (firstChunk.isReady() || firstChunk.isInProgress()) {\n+      firstChunk.poll(requestRegistrationCallback);\n+    }\n+    if (firstChunk.isComplete()) {\n+      // Although an attempt is made to write to the channel as soon as a chunk is successfully retrieved,\n+      // the caller might not have called readInto() and passed in a channel at the time. So an attempt is always\n+      // made from within poll.\n+      maybeWriteToChannel();\n+      // If this is a composite blob, poll for requests for subsequent chunks.\n+      if (dataChunks != null) {\n+        for (GetChunk dataChunk : dataChunks) {\n+          if (dataChunk.isFree() && chunkIdIterator.hasNext()) {\n+            dataChunk.initialize(chunkIdIterator.nextIndex(), (BlobId) chunkIdIterator.next());\n+          }\n+          if (dataChunk.isInProgress() || (dataChunk.isReady()\n+              && numChunksRetrieved - numChunksWrittenOut.get() < NonBlockingRouter.MAX_IN_MEM_CHUNKS)) {\n+            dataChunk.poll(requestRegistrationCallback);\n+          }\n+        }\n+      }\n+    }\n+    if (operationException.get() != null) {\n+      abort(operationException.get());\n+    }\n+  }\n+\n+  // ReadableStreamChannel implementation:\n+\n+  /**\n+   * {@inheritDoc}\n+   * <br>\n+   * The bytes that will be read from this channel is not known until the read is complete.\n+   * @return -1.\n+   */\n+  @Override\n+  public long getSize() {\n+    return -1;\n+  }\n+\n+  /**\n+   * {@inheritDoc}\n+   * <br>\n+   * @param asyncWritableChannel the {@link AsyncWritableChannel} to read the data into.\n+   * @param callback the {@link Callback} that will be invoked either when all the data in the channel has been emptied\n+   *                 into the {@code asyncWritableChannel} or if there is an exception in doing so. This can be null.\n+   * @return the {@link Future} that will eventually contain the result of the operation.\n+   */\n+  @Override\n+  public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) {","path":"ambry-router/src/main/java/com.github.ambry.router/GetBlobOperation.java","position":319,"original_position":319,"commit_id":"dd05d89b501b1d61bc06f4c6434ccdfbb9622913","original_commit_id":"dd05d89b501b1d61bc06f4c6434ccdfbb9622913","user":{"login":"xiahome","id":8271368,"avatar_url":"https://avatars.githubusercontent.com/u/8271368?v=3","gravatar_id":"","url":"https://api.github.com/users/xiahome","html_url":"https://github.com/xiahome","followers_url":"https://api.github.com/users/xiahome/followers","following_url":"https://api.github.com/users/xiahome/following{/other_user}","gists_url":"https://api.github.com/users/xiahome/gists{/gist_id}","starred_url":"https://api.github.com/users/xiahome/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xiahome/subscriptions","organizations_url":"https://api.github.com/users/xiahome/orgs","repos_url":"https://api.github.com/users/xiahome/repos","events_url":"https://api.github.com/users/xiahome/events{/privacy}","received_events_url":"https://api.github.com/users/xiahome/received_events","type":"User","site_admin":false},"body":"Who is the caller of this method?","created_at":"2016-05-18T06:29:03Z","updated_at":"2016-05-18T06:29:03Z","html_url":"https://github.com/linkedin/ambry/pull/308#discussion_r63651479","pull_request_url":"https://api.github.com/repos/linkedin/ambry/pulls/308","_links":{"self":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/comments/63651479"},"html":{"href":"https://github.com/linkedin/ambry/pull/308#discussion_r63651479"},"pull_request":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/308"}}},"pull_request":{"url":"https://api.github.com/repos/linkedin/ambry/pulls/308","id":70219239,"html_url":"https://github.com/linkedin/ambry/pull/308","diff_url":"https://github.com/linkedin/ambry/pull/308.diff","patch_url":"https://github.com/linkedin/ambry/pull/308.patch","issue_url":"https://api.github.com/repos/linkedin/ambry/issues/308","number":308,"state":"open","locked":false,"title":"Add support for getBlob() in the router","user":{"login":"pnarayanan","id":1454915,"avatar_url":"https://avatars.githubusercontent.com/u/1454915?v=3","gravatar_id":"","url":"https://api.github.com/users/pnarayanan","html_url":"https://github.com/pnarayanan","followers_url":"https://api.github.com/users/pnarayanan/followers","following_url":"https://api.github.com/users/pnarayanan/following{/other_user}","gists_url":"https://api.github.com/users/pnarayanan/gists{/gist_id}","starred_url":"https://api.github.com/users/pnarayanan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pnarayanan/subscriptions","organizations_url":"https://api.github.com/users/pnarayanan/orgs","repos_url":"https://api.github.com/users/pnarayanan/repos","events_url":"https://api.github.com/users/pnarayanan/events{/privacy}","received_events_url":"https://api.github.com/users/pnarayanan/received_events","type":"User","site_admin":false},"body":"Add support for GetBlob operations in the NonBlockingRouter. This resolves issue #296.\r\n\r\nPrimary reviewers: Siva, Gopal. Others please feel free to take a look.\r\nExpected time: ~2 hours for one pass. Hoping for a wall clock time of ~1 week for this to get merged after iterations.\r\n\r\nSome context to help review the change:\r\n\r\n- GetBlob has to support getting both simple blobs (legacy blobs and new style blobs that ended up in single chunks) and composite blobs (multi-chunk). Whether a blob is simple or composite is not known until the first chunk is fetched. Therefore, the first chunk is treated as a special chunk within `GetBlobOperation`.\r\n- The operation callback may have to be invoked before the operation completes, as the result of the get operation has to be made available as soon as the first bytes of the data arrives. Additionally, subsequent chunks may be fetched only after earlier chunk data are consumed by the caller (to limit the buffering within the router).\r\n- The result of a GetBlob operation, which is a `ReadableStreamChannel`, is implemented directly by the `GetBlobOperation`, which makes sharing states easy.\r\n- Any exceptions encountered after the operation callback is invoked will be notified via the read callback passed in by the caller as part of the `ReadableStreamChannel#readInto()`.\r\n- The `GetManager` will have to continue to poll the `GetBlobOperation` even after the latter invokes the operation callback, because subsequent chunks will have to be fetched in the context of the poll. Additionally, even if all the chunks are fetched, the operation can only complete until the `ReadableStreamChannel#readInto()` is called and all the data is written out. Until that is done, `GetBlobOperation` does not mark itself complete so that it continues to get polled by the `GetManager`.\r\n- The `GetChunk` objects are reused similar to how `PutChunk` objects are reused in `PutOperation`. However, note that chunk buffer allocations do not happen within a `GetChunk`. The `ByteBuffer` is allocated by MessageFormat when deserializing the response.\r\n- Although much of the code in `GetBlobOperation` get exercised in the context of the `RequestResponseHandler` thread, some of it gets exercised in the context of the caller, when it makes `ReadableStreamChannel#readInto()` and in the context of the invoker of the AsyncWritableChannel callback, possibly concurrently.\r\n\r\n---\r\n\r\nThe core changes are in `GetBlobOperation.java`. You will notice while reviewing that not a lot of things are in the abstract `GetOperation` class. This is because of the asymmetry between `GetBlobInfoOperation` and `GetBlobOperation`. For the latter, much of the work related to sending requests and handling response happens within the `GetChunk` objects.\r\n\r\n--\r\n*Coverage*\r\n```\r\nGetBlobOperation    100% (6/ 6)    92.7% (38/ 41)    93% (199/ 214)\r\nGetManager          100% (2/ 2)    100% (10/ 10)     90.2% (37/ 41)\r\nGetOperation        100% (1/ 1)    90% (9/ 10)       95.5% (21/ 22)\r\n```\r\nA few more tests will be added.\r\n","created_at":"2016-05-16T16:07:09Z","updated_at":"2016-05-18T06:29:03Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e79e92fddfcf438c1bf6099f5185818aca548709","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/linkedin/ambry/pulls/308/commits","review_comments_url":"https://api.github.com/repos/linkedin/ambry/pulls/308/comments","review_comment_url":"https://api.github.com/repos/linkedin/ambry/pulls/comments{/number}","comments_url":"https://api.github.com/repos/linkedin/ambry/issues/308/comments","statuses_url":"https://api.github.com/repos/linkedin/ambry/statuses/dd05d89b501b1d61bc06f4c6434ccdfbb9622913","head":{"label":"pnarayanan:GetBlob","ref":"GetBlob","sha":"dd05d89b501b1d61bc06f4c6434ccdfbb9622913","user":{"login":"pnarayanan","id":1454915,"avatar_url":"https://avatars.githubusercontent.com/u/1454915?v=3","gravatar_id":"","url":"https://api.github.com/users/pnarayanan","html_url":"https://github.com/pnarayanan","followers_url":"https://api.github.com/users/pnarayanan/followers","following_url":"https://api.github.com/users/pnarayanan/following{/other_user}","gists_url":"https://api.github.com/users/pnarayanan/gists{/gist_id}","starred_url":"https://api.github.com/users/pnarayanan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pnarayanan/subscriptions","organizations_url":"https://api.github.com/users/pnarayanan/orgs","repos_url":"https://api.github.com/users/pnarayanan/repos","events_url":"https://api.github.com/users/pnarayanan/events{/privacy}","received_events_url":"https://api.github.com/users/pnarayanan/received_events","type":"User","site_admin":false},"repo":{"id":25383889,"name":"ambry","full_name":"pnarayanan/ambry","owner":{"login":"pnarayanan","id":1454915,"avatar_url":"https://avatars.githubusercontent.com/u/1454915?v=3","gravatar_id":"","url":"https://api.github.com/users/pnarayanan","html_url":"https://github.com/pnarayanan","followers_url":"https://api.github.com/users/pnarayanan/followers","following_url":"https://api.github.com/users/pnarayanan/following{/other_user}","gists_url":"https://api.github.com/users/pnarayanan/gists{/gist_id}","starred_url":"https://api.github.com/users/pnarayanan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pnarayanan/subscriptions","organizations_url":"https://api.github.com/users/pnarayanan/orgs","repos_url":"https://api.github.com/users/pnarayanan/repos","events_url":"https://api.github.com/users/pnarayanan/events{/privacy}","received_events_url":"https://api.github.com/users/pnarayanan/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/pnarayanan/ambry","description":"Immutable blob store","fork":true,"url":"https://api.github.com/repos/pnarayanan/ambry","forks_url":"https://api.github.com/repos/pnarayanan/ambry/forks","keys_url":"https://api.github.com/repos/pnarayanan/ambry/keys{/key_id}","collaborators_url":"https://api.github.com/repos/pnarayanan/ambry/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/pnarayanan/ambry/teams","hooks_url":"https://api.github.com/repos/pnarayanan/ambry/hooks","issue_events_url":"https://api.github.com/repos/pnarayanan/ambry/issues/events{/number}","events_url":"https://api.github.com/repos/pnarayanan/ambry/events","assignees_url":"https://api.github.com/repos/pnarayanan/ambry/assignees{/user}","branches_url":"https://api.github.com/repos/pnarayanan/ambry/branches{/branch}","tags_url":"https://api.github.com/repos/pnarayanan/ambry/tags","blobs_url":"https://api.github.com/repos/pnarayanan/ambry/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/pnarayanan/ambry/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/pnarayanan/ambry/git/refs{/sha}","trees_url":"https://api.github.com/repos/pnarayanan/ambry/git/trees{/sha}","statuses_url":"https://api.github.com/repos/pnarayanan/ambry/statuses/{sha}","languages_url":"https://api.github.com/repos/pnarayanan/ambry/languages","stargazers_url":"https://api.github.com/repos/pnarayanan/ambry/stargazers","contributors_url":"https://api.github.com/repos/pnarayanan/ambry/contributors","subscribers_url":"https://api.github.com/repos/pnarayanan/ambry/subscribers","subscription_url":"https://api.github.com/repos/pnarayanan/ambry/subscription","commits_url":"https://api.github.com/repos/pnarayanan/ambry/commits{/sha}","git_commits_url":"https://api.github.com/repos/pnarayanan/ambry/git/commits{/sha}","comments_url":"https://api.github.com/repos/pnarayanan/ambry/comments{/number}","issue_comment_url":"https://api.github.com/repos/pnarayanan/ambry/issues/comments{/number}","contents_url":"https://api.github.com/repos/pnarayanan/ambry/contents/{+path}","compare_url":"https://api.github.com/repos/pnarayanan/ambry/compare/{base}...{head}","merges_url":"https://api.github.com/repos/pnarayanan/ambry/merges","archive_url":"https://api.github.com/repos/pnarayanan/ambry/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/pnarayanan/ambry/downloads","issues_url":"https://api.github.com/repos/pnarayanan/ambry/issues{/number}","pulls_url":"https://api.github.com/repos/pnarayanan/ambry/pulls{/number}","milestones_url":"https://api.github.com/repos/pnarayanan/ambry/milestones{/number}","notifications_url":"https://api.github.com/repos/pnarayanan/ambry/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/pnarayanan/ambry/labels{/name}","releases_url":"https://api.github.com/repos/pnarayanan/ambry/releases{/id}","deployments_url":"https://api.github.com/repos/pnarayanan/ambry/deployments","created_at":"2014-10-18T01:44:12Z","updated_at":"2016-05-03T23:43:02Z","pushed_at":"2016-05-17T19:59:13Z","git_url":"git://github.com/pnarayanan/ambry.git","ssh_url":"git@github.com:pnarayanan/ambry.git","clone_url":"https://github.com/pnarayanan/ambry.git","svn_url":"https://github.com/pnarayanan/ambry","homepage":null,"size":6545,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"linkedin:master","ref":"master","sha":"59974cb875c9606f35535031ffc66eec0d8cca13","user":{"login":"linkedin","id":357098,"avatar_url":"https://avatars.githubusercontent.com/u/357098?v=3","gravatar_id":"","url":"https://api.github.com/users/linkedin","html_url":"https://github.com/linkedin","followers_url":"https://api.github.com/users/linkedin/followers","following_url":"https://api.github.com/users/linkedin/following{/other_user}","gists_url":"https://api.github.com/users/linkedin/gists{/gist_id}","starred_url":"https://api.github.com/users/linkedin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/linkedin/subscriptions","organizations_url":"https://api.github.com/users/linkedin/orgs","repos_url":"https://api.github.com/users/linkedin/repos","events_url":"https://api.github.com/users/linkedin/events{/privacy}","received_events_url":"https://api.github.com/users/linkedin/received_events","type":"Organization","site_admin":false},"repo":{"id":13870893,"name":"ambry","full_name":"linkedin/ambry","owner":{"login":"linkedin","id":357098,"avatar_url":"https://avatars.githubusercontent.com/u/357098?v=3","gravatar_id":"","url":"https://api.github.com/users/linkedin","html_url":"https://github.com/linkedin","followers_url":"https://api.github.com/users/linkedin/followers","following_url":"https://api.github.com/users/linkedin/following{/other_user}","gists_url":"https://api.github.com/users/linkedin/gists{/gist_id}","starred_url":"https://api.github.com/users/linkedin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/linkedin/subscriptions","organizations_url":"https://api.github.com/users/linkedin/orgs","repos_url":"https://api.github.com/users/linkedin/repos","events_url":"https://api.github.com/users/linkedin/events{/privacy}","received_events_url":"https://api.github.com/users/linkedin/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/linkedin/ambry","description":"Distributed object store","fork":false,"url":"https://api.github.com/repos/linkedin/ambry","forks_url":"https://api.github.com/repos/linkedin/ambry/forks","keys_url":"https://api.github.com/repos/linkedin/ambry/keys{/key_id}","collaborators_url":"https://api.github.com/repos/linkedin/ambry/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/linkedin/ambry/teams","hooks_url":"https://api.github.com/repos/linkedin/ambry/hooks","issue_events_url":"https://api.github.com/repos/linkedin/ambry/issues/events{/number}","events_url":"https://api.github.com/repos/linkedin/ambry/events","assignees_url":"https://api.github.com/repos/linkedin/ambry/assignees{/user}","branches_url":"https://api.github.com/repos/linkedin/ambry/branches{/branch}","tags_url":"https://api.github.com/repos/linkedin/ambry/tags","blobs_url":"https://api.github.com/repos/linkedin/ambry/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/linkedin/ambry/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/linkedin/ambry/git/refs{/sha}","trees_url":"https://api.github.com/repos/linkedin/ambry/git/trees{/sha}","statuses_url":"https://api.github.com/repos/linkedin/ambry/statuses/{sha}","languages_url":"https://api.github.com/repos/linkedin/ambry/languages","stargazers_url":"https://api.github.com/repos/linkedin/ambry/stargazers","contributors_url":"https://api.github.com/repos/linkedin/ambry/contributors","subscribers_url":"https://api.github.com/repos/linkedin/ambry/subscribers","subscription_url":"https://api.github.com/repos/linkedin/ambry/subscription","commits_url":"https://api.github.com/repos/linkedin/ambry/commits{/sha}","git_commits_url":"https://api.github.com/repos/linkedin/ambry/git/commits{/sha}","comments_url":"https://api.github.com/repos/linkedin/ambry/comments{/number}","issue_comment_url":"https://api.github.com/repos/linkedin/ambry/issues/comments{/number}","contents_url":"https://api.github.com/repos/linkedin/ambry/contents/{+path}","compare_url":"https://api.github.com/repos/linkedin/ambry/compare/{base}...{head}","merges_url":"https://api.github.com/repos/linkedin/ambry/merges","archive_url":"https://api.github.com/repos/linkedin/ambry/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/linkedin/ambry/downloads","issues_url":"https://api.github.com/repos/linkedin/ambry/issues{/number}","pulls_url":"https://api.github.com/repos/linkedin/ambry/pulls{/number}","milestones_url":"https://api.github.com/repos/linkedin/ambry/milestones{/number}","notifications_url":"https://api.github.com/repos/linkedin/ambry/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/linkedin/ambry/labels{/name}","releases_url":"https://api.github.com/repos/linkedin/ambry/releases{/id}","deployments_url":"https://api.github.com/repos/linkedin/ambry/deployments","created_at":"2013-10-25T20:04:54Z","updated_at":"2016-05-18T06:18:19Z","pushed_at":"2016-05-18T03:18:34Z","git_url":"git://github.com/linkedin/ambry.git","ssh_url":"git@github.com:linkedin/ambry.git","clone_url":"https://github.com/linkedin/ambry.git","svn_url":"https://github.com/linkedin/ambry","homepage":"https://github.com/linkedin/ambry/wiki","size":6966,"stargazers_count":242,"watchers_count":242,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":25,"mirror_url":null,"open_issues_count":19,"forks":25,"open_issues":19,"watchers":242,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/308"},"html":{"href":"https://github.com/linkedin/ambry/pull/308"},"issue":{"href":"https://api.github.com/repos/linkedin/ambry/issues/308"},"comments":{"href":"https://api.github.com/repos/linkedin/ambry/issues/308/comments"},"review_comments":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/308/comments"},"review_comment":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/linkedin/ambry/pulls/308/commits"},"statuses":{"href":"https://api.github.com/repos/linkedin/ambry/statuses/dd05d89b501b1d61bc06f4c6434ccdfbb9622913"}}}}