{"action":"created","comment":{"url":"https://api.github.com/repos/elastic/elasticsearch/pulls/comments/60161897","id":60161897,"diff_hunk":"@@ -0,0 +1,349 @@\n+/*\n+ * Licensed to Elasticsearch under one or more contributor\n+ * license agreements. See the NOTICE file distributed with\n+ * this work for additional information regarding copyright\n+ * ownership. Elasticsearch licenses this file to you under\n+ * the Apache License, Version 2.0 (the \"License\"); you may\n+ * 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,\n+ * software distributed under the License is distributed on an\n+ * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n+ * KIND, either express or implied.  See the License for the\n+ * specific language governing permissions and limitations\n+ * under the License.\n+ */\n+package org.elasticsearch.action.support.replication;\n+\n+import org.elasticsearch.ElasticsearchException;\n+import org.elasticsearch.ExceptionsHelper;\n+import org.elasticsearch.action.ActionListener;\n+import org.elasticsearch.action.ReplicationResponse;\n+import org.elasticsearch.action.UnavailableShardsException;\n+import org.elasticsearch.action.WriteConsistencyLevel;\n+import org.elasticsearch.action.support.TransportActions;\n+import org.elasticsearch.cluster.ClusterState;\n+import org.elasticsearch.cluster.routing.IndexRoutingTable;\n+import org.elasticsearch.cluster.routing.IndexShardRoutingTable;\n+import org.elasticsearch.cluster.routing.ShardRouting;\n+import org.elasticsearch.common.collect.Tuple;\n+import org.elasticsearch.common.io.stream.StreamInput;\n+import org.elasticsearch.common.logging.ESLogger;\n+import org.elasticsearch.index.engine.VersionConflictEngineException;\n+import org.elasticsearch.index.shard.ShardId;\n+import org.elasticsearch.rest.RestStatus;\n+import org.elasticsearch.transport.TransportResponse;\n+\n+import java.io.IOException;\n+import java.util.ArrayList;\n+import java.util.Collections;\n+import java.util.List;\n+import java.util.Locale;\n+import java.util.concurrent.atomic.AtomicBoolean;\n+import java.util.concurrent.atomic.AtomicInteger;\n+import java.util.function.Consumer;\n+import java.util.function.Supplier;\n+\n+public class ReplicationOperation<Request extends ReplicationRequest<Request>, ReplicaRequest extends ReplicationRequest<ReplicaRequest>,\n+    Response extends ReplicationResponse> {\n+    final ESLogger logger;\n+    final Request request;\n+    final Supplier<ClusterState> clusterStateSupplier;\n+    final String opType;\n+    final AtomicInteger totalShards = new AtomicInteger();\n+    final AtomicInteger pendingShards = new AtomicInteger();\n+    final AtomicInteger successfulShards = new AtomicInteger();\n+    final boolean executeOnReplicas;\n+    final boolean checkWriteConsistency;\n+    final Primary<Request, ReplicaRequest, Response> primary;\n+    final Replicas<ReplicaRequest> replicasProxy;\n+    final AtomicBoolean finished = new AtomicBoolean();\n+    final ActionListener<Response> finalResponseListener;\n+\n+    volatile Response finalResponse = null;\n+\n+    private final List<ReplicationResponse.ShardInfo.Failure> shardReplicaFailures = Collections.synchronizedList(new ArrayList<>());\n+\n+\n+    ReplicationOperation(Request request, Primary<Request, ReplicaRequest, Response> primary,\n+                         ActionListener<Response> listener,\n+                         boolean executeOnReplicas, boolean checkWriteConsistency,\n+                         Replicas<ReplicaRequest> replicas,\n+                         Supplier<ClusterState> clusterStateSupplier, ESLogger logger, String opType) {\n+        this.checkWriteConsistency = checkWriteConsistency;\n+        this.executeOnReplicas = executeOnReplicas;\n+        this.replicasProxy = replicas;\n+        this.primary = primary;\n+        this.finalResponseListener = listener;\n+        this.logger = logger;\n+        this.request = request;\n+        this.clusterStateSupplier = clusterStateSupplier;\n+        this.opType = opType;\n+    }\n+\n+    void execute() throws Exception {\n+        final String writeConsistencyFailure = checkWriteConsistency ? checkWriteConsistency() : null;\n+        final ShardId shardId = primary.routingEntry().shardId();\n+        if (writeConsistencyFailure != null) {\n+            finishAsFailed(new UnavailableShardsException(shardId,\n+                \"{} Timeout: [{}], request: [{}]\", writeConsistencyFailure, request.timeout(), request));\n+            return;\n+        }\n+\n+        totalShards.incrementAndGet();\n+        pendingShards.incrementAndGet(); // increase by 1 until we finish all primary coordination\n+        Tuple<Response, ReplicaRequest> primaryResponse = primary.perform(request);\n+        successfulShards.incrementAndGet(); // mark primary as successful\n+        finalResponse = primaryResponse.v1();\n+        ReplicaRequest replicaRequest = primaryResponse.v2();\n+        assert replicaRequest.primaryTerm() > 0 : \"replicaRequest doesn't have it's primary term\" ;\n+        if (logger.isTraceEnabled()) {\n+            logger.trace(\"[{}] op [{}] completed on primary for request [{}]\", shardId, opType, request);\n+        }\n+        // we have to get a new state after successfully indexing into the primary in order to honour recovery semantics.\n+        // we have to make sure that every operation indexed into the primary after recovery start will also be replicated\n+        // to the recovery target. If we use an old cluster state, we may miss a relocation that has started since then.\n+        // If the index gets deleted after primary operation, we skip replication\n+        List<ShardRouting> shards = getShards(shardId, clusterStateSupplier.get());\n+        final String localNodeId = primary.routingEntry().currentNodeId();\n+        for (final ShardRouting shard : shards) {\n+            if (executeOnReplicas == false || shard.unassigned()) {\n+                if (shard.primary() == false) {\n+                    totalShards.incrementAndGet();\n+                }\n+                continue;\n+            }\n+\n+            if (shard.currentNodeId().equals(localNodeId) == false) {\n+                performOnReplica(shard, replicaRequest);\n+            }\n+\n+            if (shard.relocating() && shard.relocatingNodeId().equals(localNodeId) == false) {\n+                performOnReplica(shard.buildTargetRelocatingShard(), replicaRequest);\n+            }\n+        }\n+\n+        // decrement pending and finish (if there are no replicas, or those are done)\n+        decPendingAndFinishIfNeeded(); // incremented in the beginning of this method\n+    }\n+\n+    private void performOnReplica(final ShardRouting shard, final ReplicaRequest replicaRequest) {\n+        if (logger.isTraceEnabled()) {\n+            logger.trace(\"[{}] sending op [{}] to replica {} for request [{}]\", shard.shardId(), opType, shard, replicaRequest);\n+        }\n+\n+        totalShards.incrementAndGet();\n+        pendingShards.incrementAndGet();\n+        replicasProxy.performOn(shard, replicaRequest, new ActionListener<TransportResponse.Empty>() {\n+            @Override\n+            public void onResponse(TransportResponse.Empty empty) {\n+                successfulShards.incrementAndGet();\n+                decPendingAndFinishIfNeeded();\n+            }\n+\n+            @Override\n+            public void onFailure(Throwable replicaException) {\n+                logger.trace(\"[{}] failure while performing [{}] on replica {}, request [{}]\", replicaException, shard.shardId(), opType,\n+                    shard, replicaRequest);\n+                if (ignoreReplicaException(replicaException)) {\n+                    decPendingAndFinishIfNeeded();\n+                } else {\n+                    RestStatus restStatus = ExceptionsHelper.status(replicaException);\n+                    shardReplicaFailures.add(new ReplicationResponse.ShardInfo.Failure(\n+                        shard.shardId(), shard.currentNodeId(), replicaException, restStatus, false));\n+                    String message = String.format(Locale.ROOT, \"failed to perform %s on replica %s\", opType, shard);\n+                    logger.warn(\"[{}] {}\", replicaException, shard.shardId(), message);\n+                    replicasProxy.failShard(shard, primary.routingEntry(), message, replicaException,\n+                        ReplicationOperation.this::decPendingAndFinishIfNeeded,\n+                        ReplicationOperation.this::onPrimaryDemoted,\n+                        throwable -> decPendingAndFinishIfNeeded()\n+                    );\n+                }\n+            }\n+        });\n+    }\n+\n+    private void onPrimaryDemoted(Throwable demotionFailure) {\n+        String primaryFail = String.format(Locale.ROOT,\n+            \"primary shard [%s] was demoted while failing replica shard\",\n+            primary.routingEntry());\n+        // we are no longer the primary, fail ourselves and start over\n+        primary.failShard(primaryFail, demotionFailure);\n+        finishAsFailed(new RetryOnPrimaryException(primary.routingEntry().shardId(), primaryFail, demotionFailure));\n+    }\n+\n+    /**\n+     * checks whether we can perform a write based on the write consistency setting\n+     * returns **null* if OK to proceed, or a string describing the reason to stop\n+     */\n+    String checkWriteConsistency() {\n+        assert request.consistencyLevel() != WriteConsistencyLevel.DEFAULT : \"consistency level should be set\";\n+        final ShardId shardId = primary.routingEntry().shardId();\n+        final ClusterState state = clusterStateSupplier.get();\n+        final WriteConsistencyLevel consistencyLevel = request.consistencyLevel();\n+        final int sizeActive;\n+        final int requiredNumber;\n+        IndexRoutingTable indexRoutingTable = state.getRoutingTable().index(shardId.getIndexName());\n+        if (indexRoutingTable != null) {\n+            IndexShardRoutingTable shardRoutingTable = indexRoutingTable.shard(shardId.getId());\n+            if (shardRoutingTable != null) {\n+                sizeActive = shardRoutingTable.activeShards().size();\n+                if (consistencyLevel == WriteConsistencyLevel.QUORUM && shardRoutingTable.getSize() > 2) {\n+                    // only for more than 2 in the number of shardIt it makes sense, otherwise its 1 shard with 1 replica,\n+                    // quorum is 1 (which is what it is initialized to)\n+                    requiredNumber = (shardRoutingTable.getSize() / 2) + 1;\n+                } else if (consistencyLevel == WriteConsistencyLevel.ALL) {\n+                    requiredNumber = shardRoutingTable.getSize();\n+                } else {\n+                    requiredNumber = 1;\n+                }\n+            } else {\n+                sizeActive = 0;\n+                requiredNumber = 1;\n+            }\n+        } else {\n+            sizeActive = 0;\n+            requiredNumber = 1;\n+        }\n+\n+        if (sizeActive < requiredNumber) {\n+            logger.trace(\"[{}] not enough active copies to meet write consistency of [{}] (have {}, needed {}), scheduling a retry.\" +\n+                \" op [{}], request [{}]\", shardId, consistencyLevel, sizeActive, requiredNumber, opType, request);\n+            return \"Not enough active copies to meet write consistency of [\" + consistencyLevel + \"] (have \" + sizeActive + \", needed \"\n+                + requiredNumber + \").\";\n+        } else {\n+            return null;\n+        }\n+    }\n+\n+    protected List<ShardRouting> getShards(ShardId shardId, ClusterState state) {\n+        // can be null if the index is deleted / closed on us..\n+        final IndexShardRoutingTable shardRoutingTable = state.getRoutingTable().shardRoutingTableOrNull(shardId);\n+        List<ShardRouting> shards = shardRoutingTable == null ? Collections.emptyList() : shardRoutingTable.shards();\n+        return shards;\n+    }\n+\n+    private void decPendingAndFinishIfNeeded() {\n+        assert pendingShards.get() > 0;\n+        if (pendingShards.decrementAndGet() == 0) {\n+            finish();\n+        }\n+    }\n+\n+    private void finish() {\n+        if (finished.compareAndSet(false, true)) {\n+            final ReplicationResponse.ShardInfo.Failure[] failuresArray;\n+            if (shardReplicaFailures.isEmpty()) {\n+                failuresArray = ReplicationResponse.EMPTY;\n+            } else {\n+                failuresArray = new ReplicationResponse.ShardInfo.Failure[shardReplicaFailures.size()];\n+                shardReplicaFailures.toArray(failuresArray);\n+            }\n+            finalResponse.setShardInfo(new ReplicationResponse.ShardInfo(\n+                    totalShards.get(),\n+                    successfulShards.get(),\n+                    failuresArray\n+                )\n+            );\n+            finalResponseListener.onResponse(finalResponse);\n+        }\n+    }\n+\n+    private void finishAsFailed(Throwable throwable) {\n+        if (finished.compareAndSet(false, true)) {\n+            finalResponseListener.onFailure(throwable);\n+        }\n+    }\n+\n+\n+    /**\n+     * Should an exception be ignored when the operation is performed on the replica.\n+     */\n+    public static boolean ignoreReplicaException(Throwable e) {\n+        if (TransportActions.isShardNotAvailableException(e)) {\n+            return true;\n+        }\n+        // on version conflict or document missing, it means\n+        // that a new change has crept into the replica, and it's fine\n+        if (isConflictException(e)) {\n+            return true;\n+        }\n+        return false;\n+    }\n+\n+    public static boolean isConflictException(Throwable e) {\n+        Throwable cause = ExceptionsHelper.unwrapCause(e);\n+        // on version conflict or document missing, it means\n+        // that a new change has crept into the replica, and it's fine\n+        if (cause instanceof VersionConflictEngineException) {\n+            return true;\n+        }\n+        return false;\n+    }\n+\n+\n+    interface Primary<Request extends ReplicationRequest<Request>, ReplicaRequest extends ReplicationRequest<ReplicaRequest>,\n+        Response extends ReplicationResponse> {\n+\n+        /** routing entry for this primary */\n+        ShardRouting routingEntry();\n+\n+        /** fail the primary, typically due to the fact that the operation has learned the primary has been demoted by the master */\n+        void failShard(String message, Throwable throwable);\n+\n+        /**\n+         * Performs the given request on this primary\n+         *\n+         * @return A tuple containing not null values, as first value the result of the primary operation and as second value\n+         * the request to be executed on the replica shards.\n+         */\n+        Tuple<Response, ReplicaRequest> perform(Request request) throws Exception;\n+\n+    }\n+\n+    interface Replicas<ReplicaRequest extends ReplicationRequest<ReplicaRequest>> {\n+\n+        /**\n+         * performs the the given request on the specified replica\n+         *\n+         * @param replica {@link ShardRouting} of the shard this request should be executed on\n+         * @param replicaRequest operation to peform\n+         * @param listener a callback to call once the operation has been complicated, either successfully or with an error.\n+         */\n+        void performOn(ShardRouting replica, ReplicaRequest replicaRequest, ActionListener<TransportResponse.Empty> listener);\n+\n+        /**\n+         * Fail the specified shard, removing it from the current set of active shards\n+         * @param replica shard to fail\n+         * @param primary the primary shard that requested the failure\n+         * @param message a (short) description of the reason\n+         * @param throwable the original exception which caused the ReplicationOperation to request the shard to be failed\n+         * @param onSuccess a call back to call when the shard has been successfully removed from the active set.","path":"core/src/main/java/org/elasticsearch/action/support/replication/ReplicationOperation.java","position":324,"original_position":324,"commit_id":"87e1f6d0c67788693c048dc1414b2a5534268241","original_commit_id":"87e1f6d0c67788693c048dc1414b2a5534268241","user":{"login":"jasontedor","id":4744941,"avatar_url":"https://avatars.githubusercontent.com/u/4744941?v=3","gravatar_id":"","url":"https://api.github.com/users/jasontedor","html_url":"https://github.com/jasontedor","followers_url":"https://api.github.com/users/jasontedor/followers","following_url":"https://api.github.com/users/jasontedor/following{/other_user}","gists_url":"https://api.github.com/users/jasontedor/gists{/gist_id}","starred_url":"https://api.github.com/users/jasontedor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jasontedor/subscriptions","organizations_url":"https://api.github.com/users/jasontedor/orgs","repos_url":"https://api.github.com/users/jasontedor/repos","events_url":"https://api.github.com/users/jasontedor/events{/privacy}","received_events_url":"https://api.github.com/users/jasontedor/received_events","type":"User","site_admin":false},"body":"Nit: `\"call back\"` -> `\"callback\"`","created_at":"2016-04-19T01:28:22Z","updated_at":"2016-04-19T01:28:22Z","html_url":"https://github.com/elastic/elasticsearch/pull/16492#discussion_r60161897","pull_request_url":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492","_links":{"self":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/comments/60161897"},"html":{"href":"https://github.com/elastic/elasticsearch/pull/16492#discussion_r60161897"},"pull_request":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492"}}},"pull_request":{"url":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492","id":58558499,"html_url":"https://github.com/elastic/elasticsearch/pull/16492","diff_url":"https://github.com/elastic/elasticsearch/pull/16492.diff","patch_url":"https://github.com/elastic/elasticsearch/pull/16492.patch","issue_url":"https://api.github.com/repos/elastic/elasticsearch/issues/16492","number":16492,"state":"open","locked":false,"title":"WIP: Extract non-transport logic from TransportReplicationAction","user":{"login":"bleskes","id":1006375,"avatar_url":"https://avatars.githubusercontent.com/u/1006375?v=3","gravatar_id":"","url":"https://api.github.com/users/bleskes","html_url":"https://github.com/bleskes","followers_url":"https://api.github.com/users/bleskes/followers","following_url":"https://api.github.com/users/bleskes/following{/other_user}","gists_url":"https://api.github.com/users/bleskes/gists{/gist_id}","starred_url":"https://api.github.com/users/bleskes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bleskes/subscriptions","organizations_url":"https://api.github.com/users/bleskes/orgs","repos_url":"https://api.github.com/users/bleskes/repos","events_url":"https://api.github.com/users/bleskes/events{/privacy}","received_events_url":"https://api.github.com/users/bleskes/received_events","type":"User","site_admin":false},"body":"Recent refactoring in TransportReplicationAction has moved all logic to clear units. We can take an extra step and extract those classes so they can be tested (and added to other tests) without requiring a (fake) transport layer and support.\r\n\r\nThis is a POC pending feedback. The usage of the new classes is shown in TransportReplicationAction2 - which is now nothing but network glue. Also, TransportIndexAction2 is an example implementation. Note how that class contains it's own IndexShardReference which is in charge of doing the actual work. This allows us in the future to extract that class and use it to setup a unit test with multiple shards connected by the replication logic, but without the transport and other unneeded dependencies.\r\n\r\n@s1monw @jasontedor @ywelsch @areek I would like to get your feedback on this.","created_at":"2016-02-07T21:53:21Z","updated_at":"2016-04-19T01:28:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f78397823dae9170b7ef5a8b4319c3666d89622e","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492/commits","review_comments_url":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492/comments","review_comment_url":"https://api.github.com/repos/elastic/elasticsearch/pulls/comments{/number}","comments_url":"https://api.github.com/repos/elastic/elasticsearch/issues/16492/comments","statuses_url":"https://api.github.com/repos/elastic/elasticsearch/statuses/87e1f6d0c67788693c048dc1414b2a5534268241","head":{"label":"bleskes:replication_the_great_recouple","ref":"replication_the_great_recouple","sha":"87e1f6d0c67788693c048dc1414b2a5534268241","user":{"login":"bleskes","id":1006375,"avatar_url":"https://avatars.githubusercontent.com/u/1006375?v=3","gravatar_id":"","url":"https://api.github.com/users/bleskes","html_url":"https://github.com/bleskes","followers_url":"https://api.github.com/users/bleskes/followers","following_url":"https://api.github.com/users/bleskes/following{/other_user}","gists_url":"https://api.github.com/users/bleskes/gists{/gist_id}","starred_url":"https://api.github.com/users/bleskes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bleskes/subscriptions","organizations_url":"https://api.github.com/users/bleskes/orgs","repos_url":"https://api.github.com/users/bleskes/repos","events_url":"https://api.github.com/users/bleskes/events{/privacy}","received_events_url":"https://api.github.com/users/bleskes/received_events","type":"User","site_admin":false},"repo":{"id":9219509,"name":"elasticsearch","full_name":"bleskes/elasticsearch","owner":{"login":"bleskes","id":1006375,"avatar_url":"https://avatars.githubusercontent.com/u/1006375?v=3","gravatar_id":"","url":"https://api.github.com/users/bleskes","html_url":"https://github.com/bleskes","followers_url":"https://api.github.com/users/bleskes/followers","following_url":"https://api.github.com/users/bleskes/following{/other_user}","gists_url":"https://api.github.com/users/bleskes/gists{/gist_id}","starred_url":"https://api.github.com/users/bleskes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bleskes/subscriptions","organizations_url":"https://api.github.com/users/bleskes/orgs","repos_url":"https://api.github.com/users/bleskes/repos","events_url":"https://api.github.com/users/bleskes/events{/privacy}","received_events_url":"https://api.github.com/users/bleskes/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/bleskes/elasticsearch","description":"Open Source, Distributed, RESTful Search Engine","fork":true,"url":"https://api.github.com/repos/bleskes/elasticsearch","forks_url":"https://api.github.com/repos/bleskes/elasticsearch/forks","keys_url":"https://api.github.com/repos/bleskes/elasticsearch/keys{/key_id}","collaborators_url":"https://api.github.com/repos/bleskes/elasticsearch/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/bleskes/elasticsearch/teams","hooks_url":"https://api.github.com/repos/bleskes/elasticsearch/hooks","issue_events_url":"https://api.github.com/repos/bleskes/elasticsearch/issues/events{/number}","events_url":"https://api.github.com/repos/bleskes/elasticsearch/events","assignees_url":"https://api.github.com/repos/bleskes/elasticsearch/assignees{/user}","branches_url":"https://api.github.com/repos/bleskes/elasticsearch/branches{/branch}","tags_url":"https://api.github.com/repos/bleskes/elasticsearch/tags","blobs_url":"https://api.github.com/repos/bleskes/elasticsearch/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/bleskes/elasticsearch/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/bleskes/elasticsearch/git/refs{/sha}","trees_url":"https://api.github.com/repos/bleskes/elasticsearch/git/trees{/sha}","statuses_url":"https://api.github.com/repos/bleskes/elasticsearch/statuses/{sha}","languages_url":"https://api.github.com/repos/bleskes/elasticsearch/languages","stargazers_url":"https://api.github.com/repos/bleskes/elasticsearch/stargazers","contributors_url":"https://api.github.com/repos/bleskes/elasticsearch/contributors","subscribers_url":"https://api.github.com/repos/bleskes/elasticsearch/subscribers","subscription_url":"https://api.github.com/repos/bleskes/elasticsearch/subscription","commits_url":"https://api.github.com/repos/bleskes/elasticsearch/commits{/sha}","git_commits_url":"https://api.github.com/repos/bleskes/elasticsearch/git/commits{/sha}","comments_url":"https://api.github.com/repos/bleskes/elasticsearch/comments{/number}","issue_comment_url":"https://api.github.com/repos/bleskes/elasticsearch/issues/comments{/number}","contents_url":"https://api.github.com/repos/bleskes/elasticsearch/contents/{+path}","compare_url":"https://api.github.com/repos/bleskes/elasticsearch/compare/{base}...{head}","merges_url":"https://api.github.com/repos/bleskes/elasticsearch/merges","archive_url":"https://api.github.com/repos/bleskes/elasticsearch/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/bleskes/elasticsearch/downloads","issues_url":"https://api.github.com/repos/bleskes/elasticsearch/issues{/number}","pulls_url":"https://api.github.com/repos/bleskes/elasticsearch/pulls{/number}","milestones_url":"https://api.github.com/repos/bleskes/elasticsearch/milestones{/number}","notifications_url":"https://api.github.com/repos/bleskes/elasticsearch/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/bleskes/elasticsearch/labels{/name}","releases_url":"https://api.github.com/repos/bleskes/elasticsearch/releases{/id}","deployments_url":"https://api.github.com/repos/bleskes/elasticsearch/deployments","created_at":"2013-04-04T14:06:17Z","updated_at":"2015-04-24T10:42:15Z","pushed_at":"2016-04-18T11:33:17Z","git_url":"git://github.com/bleskes/elasticsearch.git","ssh_url":"git@github.com:bleskes/elasticsearch.git","clone_url":"https://github.com/bleskes/elasticsearch.git","svn_url":"https://github.com/bleskes/elasticsearch","homepage":"http://elasticsearch.org","size":209627,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":false,"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":"elastic:master","ref":"master","sha":"d3b130606932f8664e928f67c032c2fb796547e7","user":{"login":"elastic","id":6764390,"avatar_url":"https://avatars.githubusercontent.com/u/6764390?v=3","gravatar_id":"","url":"https://api.github.com/users/elastic","html_url":"https://github.com/elastic","followers_url":"https://api.github.com/users/elastic/followers","following_url":"https://api.github.com/users/elastic/following{/other_user}","gists_url":"https://api.github.com/users/elastic/gists{/gist_id}","starred_url":"https://api.github.com/users/elastic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/elastic/subscriptions","organizations_url":"https://api.github.com/users/elastic/orgs","repos_url":"https://api.github.com/users/elastic/repos","events_url":"https://api.github.com/users/elastic/events{/privacy}","received_events_url":"https://api.github.com/users/elastic/received_events","type":"Organization","site_admin":false},"repo":{"id":507775,"name":"elasticsearch","full_name":"elastic/elasticsearch","owner":{"login":"elastic","id":6764390,"avatar_url":"https://avatars.githubusercontent.com/u/6764390?v=3","gravatar_id":"","url":"https://api.github.com/users/elastic","html_url":"https://github.com/elastic","followers_url":"https://api.github.com/users/elastic/followers","following_url":"https://api.github.com/users/elastic/following{/other_user}","gists_url":"https://api.github.com/users/elastic/gists{/gist_id}","starred_url":"https://api.github.com/users/elastic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/elastic/subscriptions","organizations_url":"https://api.github.com/users/elastic/orgs","repos_url":"https://api.github.com/users/elastic/repos","events_url":"https://api.github.com/users/elastic/events{/privacy}","received_events_url":"https://api.github.com/users/elastic/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/elastic/elasticsearch","description":"Open Source, Distributed, RESTful Search Engine","fork":false,"url":"https://api.github.com/repos/elastic/elasticsearch","forks_url":"https://api.github.com/repos/elastic/elasticsearch/forks","keys_url":"https://api.github.com/repos/elastic/elasticsearch/keys{/key_id}","collaborators_url":"https://api.github.com/repos/elastic/elasticsearch/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/elastic/elasticsearch/teams","hooks_url":"https://api.github.com/repos/elastic/elasticsearch/hooks","issue_events_url":"https://api.github.com/repos/elastic/elasticsearch/issues/events{/number}","events_url":"https://api.github.com/repos/elastic/elasticsearch/events","assignees_url":"https://api.github.com/repos/elastic/elasticsearch/assignees{/user}","branches_url":"https://api.github.com/repos/elastic/elasticsearch/branches{/branch}","tags_url":"https://api.github.com/repos/elastic/elasticsearch/tags","blobs_url":"https://api.github.com/repos/elastic/elasticsearch/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/elastic/elasticsearch/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/elastic/elasticsearch/git/refs{/sha}","trees_url":"https://api.github.com/repos/elastic/elasticsearch/git/trees{/sha}","statuses_url":"https://api.github.com/repos/elastic/elasticsearch/statuses/{sha}","languages_url":"https://api.github.com/repos/elastic/elasticsearch/languages","stargazers_url":"https://api.github.com/repos/elastic/elasticsearch/stargazers","contributors_url":"https://api.github.com/repos/elastic/elasticsearch/contributors","subscribers_url":"https://api.github.com/repos/elastic/elasticsearch/subscribers","subscription_url":"https://api.github.com/repos/elastic/elasticsearch/subscription","commits_url":"https://api.github.com/repos/elastic/elasticsearch/commits{/sha}","git_commits_url":"https://api.github.com/repos/elastic/elasticsearch/git/commits{/sha}","comments_url":"https://api.github.com/repos/elastic/elasticsearch/comments{/number}","issue_comment_url":"https://api.github.com/repos/elastic/elasticsearch/issues/comments{/number}","contents_url":"https://api.github.com/repos/elastic/elasticsearch/contents/{+path}","compare_url":"https://api.github.com/repos/elastic/elasticsearch/compare/{base}...{head}","merges_url":"https://api.github.com/repos/elastic/elasticsearch/merges","archive_url":"https://api.github.com/repos/elastic/elasticsearch/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/elastic/elasticsearch/downloads","issues_url":"https://api.github.com/repos/elastic/elasticsearch/issues{/number}","pulls_url":"https://api.github.com/repos/elastic/elasticsearch/pulls{/number}","milestones_url":"https://api.github.com/repos/elastic/elasticsearch/milestones{/number}","notifications_url":"https://api.github.com/repos/elastic/elasticsearch/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/elastic/elasticsearch/labels{/name}","releases_url":"https://api.github.com/repos/elastic/elasticsearch/releases{/id}","deployments_url":"https://api.github.com/repos/elastic/elasticsearch/deployments","created_at":"2010-02-08T13:20:56Z","updated_at":"2016-04-18T22:13:49Z","pushed_at":"2016-04-19T01:23:30Z","git_url":"git://github.com/elastic/elasticsearch.git","ssh_url":"git@github.com:elastic/elasticsearch.git","clone_url":"https://github.com/elastic/elasticsearch.git","svn_url":"https://github.com/elastic/elasticsearch","homepage":"https://www.elastic.co/products/elasticsearch","size":307934,"stargazers_count":16050,"watchers_count":16050,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":5390,"mirror_url":null,"open_issues_count":1121,"forks":5390,"open_issues":1121,"watchers":16050,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492"},"html":{"href":"https://github.com/elastic/elasticsearch/pull/16492"},"issue":{"href":"https://api.github.com/repos/elastic/elasticsearch/issues/16492"},"comments":{"href":"https://api.github.com/repos/elastic/elasticsearch/issues/16492/comments"},"review_comments":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492/comments"},"review_comment":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/elastic/elasticsearch/pulls/16492/commits"},"statuses":{"href":"https://api.github.com/repos/elastic/elasticsearch/statuses/87e1f6d0c67788693c048dc1414b2a5534268241"}}}}