{"action":"created","comment":{"url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/comments/80526921","id":80526921,"diff_hunk":"@@ -0,0 +1,248 @@\n+import logging\n+\n+from bubuku.features.rebalance import BaseRebalanceChange\n+from bubuku.features.rebalance.broker import BrokerDescription\n+from bubuku.zookeeper import BukuExhibitor\n+\n+_LOG = logging.getLogger('bubuku.features.rebalance')\n+\n+\n+def distribute(amount, items, weight_key, apply_func):\n+    if not items:\n+        return\n+    items = sorted(items, key=weight_key, reverse=True)\n+    ceil_amount = int(amount / len(items))\n+    amounts = [ceil_amount for _ in range(0, len(items))]\n+    for idx in range(0, min(amount - sum(amounts), len(items))):\n+        amounts[idx] += 1\n+    for idx in range(0, len(items)):\n+        apply_func(items[idx], amounts[idx])\n+\n+\n+class OptimizedRebalanceChange(BaseRebalanceChange):\n+    _LOAD_STATE = 'load_state'\n+    _COMPUTE_LEADERS = 'compute_leaders'\n+    _COMPUTE_REPLICAS = 'compute_replicas'\n+    _SORT_ACTIONS = 'sort_actions'\n+    _BALANCE = 'balance'\n+\n+    def __init__(self, zk: BukuExhibitor, broker_ids: list):\n+        self.zk = zk\n+        self.broker_ids = sorted(int(id_) for id_ in broker_ids)\n+        self.broker_distribution = None\n+        self.source_distribution = None\n+        self.action_queue = None\n+        self.state = OptimizedRebalanceChange._LOAD_STATE\n+\n+    def __str__(self):\n+        return 'OptimizedRebalance state={}, queue_size={}'.format(\n+            self.state, len(self.action_queue) if self.action_queue is not None else None)\n+\n+    def run(self, current_actions) -> bool:\n+        # Stop rebalance if someone is restarting\n+        if self.should_be_paused(current_actions):\n+            _LOG.warning(\"Rebalance paused, because other blocking events running: {}\".format(current_actions))\n+            return True\n+        if self.zk.is_rebalancing():\n+            return True\n+        new_broker_ids = sorted(int(id_) for id_ in self.zk.get_broker_ids())\n+        if new_broker_ids != self.broker_ids:\n+            _LOG.warning(\"Rebalance stopped because of broker list change from {} to {}\".format(\n+                self.broker_ids, new_broker_ids))\n+            return False\n+        if self.state == OptimizedRebalanceChange._LOAD_STATE:\n+            self._load_data()\n+            self.state = OptimizedRebalanceChange._COMPUTE_LEADERS\n+        elif self.state == OptimizedRebalanceChange._COMPUTE_LEADERS:\n+            self.action_queue = self.rebalance_leaders(self.broker_distribution)\n+            self.state = OptimizedRebalanceChange._COMPUTE_REPLICAS\n+        elif self.state == OptimizedRebalanceChange._COMPUTE_REPLICAS:\n+            self._rebalance_replicas()\n+            self.state = OptimizedRebalanceChange._SORT_ACTIONS\n+        elif self.state == OptimizedRebalanceChange._SORT_ACTIONS:\n+            self.action_queue = self._sort_actions()\n+            self.state = OptimizedRebalanceChange._BALANCE\n+        elif self.state == OptimizedRebalanceChange._BALANCE:\n+            return not self._balance()\n+        return True\n+\n+    def _balance(self):\n+        if not self.action_queue:\n+            return True\n+        key, replicas = self.action_queue.popitem()\n+        topic, partition = key\n+        if not self.zk.reallocate_partition(topic, partition, replicas):\n+            self.action_queue[key] = replicas\n+        return False\n+\n+    def _rebalance_replicas(self):\n+        # Remove duplicates\n+        self._remove_replica_copies()\n+        if not self._rebalance_replicas_template(False):\n+            # It may happen, that data can not be copied, because there is leader of this topic already there.\n+            self._rebalance_replicas_template(True)\n+            if not self._rebalance_replicas_template(False):\n+                _LOG.error('Failed to rebalance replicas... Reason is unclear, will just stop the process')\n+                raise Exception('Failed to perform replica rebalance {}, {}, {}'.format(\n+                    self.broker_distribution, self.broker_ids, self.action_queue))\n+\n+    def _sort_actions(self):\n+        result = {}\n+        for topic_partition, source_broker_id, target_broker_id in self.action_queue:\n+            # Here comes small trick. Each action includes only one replacement.\n+            if topic_partition not in result:\n+                result[topic_partition] = list(self.source_distribution[topic_partition])\n+            tmp_result = result[topic_partition]\n+            for i in range(len(tmp_result) - 1, -1, -1):\n+                if tmp_result[i] == source_broker_id:\n+                    tmp_result[i] = target_broker_id\n+                    break\n+        return result\n+\n+    def _list_active_brokers_with_skip(self, skip_id: int):\n+        # Method was created only in optimization purposes\n+        return [b for b in self.broker_distribution.values() if\n+                b.broker_id != skip_id and b.broker_id in self.broker_ids]\n+\n+    def _remove_replica_copies(self):\n+        for broker in self.broker_distribution.values():\n+            # List all brokers, without free_replica_slots optimization.\n+            for topic_partition in broker.list_replica_copies():\n+                success = self._move_partition_to_replica(\n+                    broker, topic_partition,\n+                    [b for b in self._list_active_brokers_with_skip(broker.broker_id) if b.has_free_replica_slots()])\n+                if not success:\n+                    self._move_partition_to_replica(\n+                        broker, topic_partition, self._list_active_brokers_with_skip(broker.broker_id))\n+\n+    def _move_partition_to_replica(self, broker, topic_partition, broker_iterable):\n+        for target in broker_iterable:\n+            if target.accept_replica(broker, topic_partition):\n+                self.action_queue.append((topic_partition, broker.broker_id, target.broker_id))\n+                return True\n+        return False\n+\n+    def _rebalance_replicas_template(self, force: bool):\n+        for broker in self.broker_distribution.values():\n+            to_move = broker.get_replica_overload()\n+            if to_move <= 0:\n+                continue\n+            targets = self._list_active_brokers_with_skip(broker.broker_id)\n+            if not force:\n+                targets = [t for t in targets if t.has_free_replica_slots()]\n+            for _ in range(0, to_move):\n+                copied = False\n+                for topic_partition in broker.list_replicas():\n+                    if self._move_partition_to_replica(broker, topic_partition, targets):\n+                        copied = True\n+                        break\n+                if not copied:\n+                    return False\n+                if not force:\n+                    targets = [t for t in targets if t.has_free_replica_slots()]\n+        return True\n+\n+    @staticmethod\n+    def _build_leader_distribution_weights(brokers: dict):\n+        candidates = {}\n+        for source_broker_id, source_broker in brokers.items():\n+            if not source_broker.have_extra_leaders():\n+                continue\n+            for target_broker_id, target_broker in brokers.items():\n+                if not target_broker.have_less_leaders():\n+                    continue\n+                weight_list = []\n+                for topic in source_broker.topic_cardinality.keys():\n+                    delta = source_broker.topic_cardinality[topic] - target_broker.topic_cardinality.get(topic, 0)\n+                    weight_list.append((topic, delta))\n+                # Now the first element is the first to rebalance.\n+                if weight_list:\n+                    candidates[(source_broker_id, target_broker_id)] = sorted(\n+                        weight_list, key=lambda x: x[1], reverse=True)\n+        return candidates\n+\n+    @staticmethod\n+    def _place_to_cardinalities(array: list, topic: str, cardinality_change: int):\n+        old_cardinality = None\n+        for item in [(a, b) for a, b in array if a == topic]:\n+            old_cardinality = item[1]\n+            array.remove(item)\n+        if old_cardinality is None:\n+            return\n+        new_cardinality = old_cardinality + cardinality_change\n+        idx = len(array)\n+        for i in range(idx - 1, -1, -1):\n+            if array[i][1] >= new_cardinality:\n+                idx = i + 1\n+                break\n+        array.insert(idx, (topic, new_cardinality))\n+\n+    @staticmethod\n+    def rebalance_leaders(brokers: dict) -> list:\n+        candidates = OptimizedRebalanceChange._build_leader_distribution_weights(brokers)\n+        queue_list = []\n+        while any([broker.have_extra_leaders() for broker in brokers.values()]):\n+            top_candidate = None\n+            for broker_pair, weight_list in candidates.items():\n+                if not top_candidate:\n+                    top_candidate = broker_pair\n+                else:\n+                    if weight_list[0][1] > candidates[top_candidate][0][1]:\n+                        top_candidate = broker_pair\n+            # Taking best topic to move\n+            topic, _ = candidates[top_candidate][0]\n+            # Updating cardinality map\n+            for broker_pair, weight_list in candidates.items():\n+                if top_candidate == broker_pair:\n+                    OptimizedRebalanceChange._place_to_cardinalities(weight_list, topic, -2)\n+                elif top_candidate[0] == broker_pair[0] or top_candidate[1] == broker_pair[1]:  # src or dst matches\n+                    OptimizedRebalanceChange._place_to_cardinalities(weight_list, topic, -1)\n+            # Get broker objects\n+            source_broker, target_broker = brokers[top_candidate[0]], brokers[top_candidate[1]]\n+\n+            # Selecting best partition to move (It is better to just swap leadership, instead of copying data)\n+            selected_partition = None\n+            source_partitions = source_broker.list_partitions(topic, False)\n+            target_partitions = target_broker.list_partitions(topic, True)\n+            for partition in source_partitions:\n+                if partition in target_partitions:\n+                    selected_partition = partition\n+                    break\n+            # Failed to find optimized partition for swapping, taking any.\n+            selected_partition = source_partitions[0] if selected_partition is None else selected_partition\n+            topic_partition = (topic, selected_partition)\n+            # Update brokers state\n+            target_broker.accept_leader(source_broker, topic_partition)\n+            # add transfer to queue\n+            queue_list.append((topic_partition, source_broker.broker_id, target_broker.broker_id))\n+            # Remove empty lists\n+            for bp in [bp for bp, wl in candidates.items() if\n+                       not brokers[bp[0]].have_extra_leaders() or not brokers[bp[1]].have_less_leaders()]:\n+                del candidates[bp]\n+        return queue_list\n+\n+    def _load_data(self):\n+        self.broker_distribution = {id_: BrokerDescription(id_) for id_ in self.broker_ids}\n+        self.source_distribution = {(topic, partition): replicas for topic, partition, replicas in\n+                                    self.zk.load_partition_assignment()}\n+        for topic_partition, replicas in self.source_distribution.items():\n+            if not replicas:\n+                continue\n+            first = True\n+            for replica in replicas:\n+                if replica not in self.broker_distribution:\n+                    self.broker_distribution[replica] = BrokerDescription(replica)\n+                self.broker_distribution[replica].add_partition(first, topic_partition)\n+                first = False\n+        active_brokers = [broker for id_, broker in self.broker_distribution.items() if id_ in self.broker_ids]\n+        distribute(\n+            sum(b.get_leader_count() for b in self.broker_distribution.values()),\n+            active_brokers, BrokerDescription.get_leader_count, BrokerDescription.set_leader_expectation)\n+        # Here is one trick. Across all the code replica list does not contain leaders.\n+        # But in order to balance correctly (max(diff(replica+leaders) <= 1) we should add leaders and substract it\n+        # again\n+        distribute(\n+            sum((b.get_replica_count() + b.get_leader_count()) for b in self.broker_distribution.values()),\n+            active_brokers,\n+            BrokerDescription.get_replica_count,\n+            lambda x, y: x.set_replica_expectation(y - x.get_expected_leaders()))","path":"bubuku/features/rebalance/change.py","position":248,"original_position":248,"commit_id":"f0cb48e35650e24d067f4952155d808b393ccd90","original_commit_id":"f0cb48e35650e24d067f4952155d808b393ccd90","user":{"login":"rcillo","id":344648,"avatar_url":"https://avatars.githubusercontent.com/u/344648?v=3","gravatar_id":"","url":"https://api.github.com/users/rcillo","html_url":"https://github.com/rcillo","followers_url":"https://api.github.com/users/rcillo/followers","following_url":"https://api.github.com/users/rcillo/following{/other_user}","gists_url":"https://api.github.com/users/rcillo/gists{/gist_id}","starred_url":"https://api.github.com/users/rcillo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rcillo/subscriptions","organizations_url":"https://api.github.com/users/rcillo/orgs","repos_url":"https://api.github.com/users/rcillo/repos","events_url":"https://api.github.com/users/rcillo/events{/privacy}","received_events_url":"https://api.github.com/users/rcillo/received_events","type":"User","site_admin":false},"body":"what's `x` and `y`? If I understood the commentary correctly that it's just \"undoing\" something. With the goal of having the distribution of replicas correctly you need to subtract the amount of leaders. Maybe it would be more clear to remove this `lambda` as parameter and just iterate over the collection again but this time removing items, so you could have an intermediate collection with the \"all partitions distribution\" and you could obtain the \"replicas distribution\" by subtracting the \"leader distribution\". That would be easier to understand for me.","created_at":"2016-09-26T17:26:43Z","updated_at":"2016-09-26T17:26:43Z","html_url":"https://github.com/zalando-incubator/bubuku/pull/66#discussion_r80526921","pull_request_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66","_links":{"self":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/comments/80526921"},"html":{"href":"https://github.com/zalando-incubator/bubuku/pull/66#discussion_r80526921"},"pull_request":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66"}}},"pull_request":{"url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66","id":85992829,"html_url":"https://github.com/zalando-incubator/bubuku/pull/66","diff_url":"https://github.com/zalando-incubator/bubuku/pull/66.diff","patch_url":"https://github.com/zalando-incubator/bubuku/pull/66.patch","issue_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/66","number":66,"state":"open","locked":false,"title":"#63 Rebalance optimization","user":{"login":"antban","id":2656223,"avatar_url":"https://avatars.githubusercontent.com/u/2656223?v=3","gravatar_id":"","url":"https://api.github.com/users/antban","html_url":"https://github.com/antban","followers_url":"https://api.github.com/users/antban/followers","following_url":"https://api.github.com/users/antban/following{/other_user}","gists_url":"https://api.github.com/users/antban/gists{/gist_id}","starred_url":"https://api.github.com/users/antban/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/antban/subscriptions","organizations_url":"https://api.github.com/users/antban/orgs","repos_url":"https://api.github.com/users/antban/repos","events_url":"https://api.github.com/users/antban/events{/privacy}","received_events_url":"https://api.github.com/users/antban/received_events","type":"User","site_admin":false},"body":"Fully optimized rebalancing. At the end:\r\nmax(replica difference) = 1,\r\nmax(leader_difference) = 1\r\nmax(leader count for any topic)=1 (this part works only if all was balanced or all brokers was restarted one by one)\r\n\r\nAdded tests for that","created_at":"2016-09-20T14:57:30Z","updated_at":"2016-09-26T17:48:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"00164d8a630cef266028093f8adeeecaf9e2400d","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66/commits","review_comments_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66/comments","review_comment_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/comments{/number}","comments_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/66/comments","statuses_url":"https://api.github.com/repos/zalando-incubator/bubuku/statuses/f0cb48e35650e24d067f4952155d808b393ccd90","head":{"label":"zalando-incubator:I63","ref":"I63","sha":"f0cb48e35650e24d067f4952155d808b393ccd90","user":{"login":"zalando-incubator","id":18533786,"avatar_url":"https://avatars.githubusercontent.com/u/18533786?v=3","gravatar_id":"","url":"https://api.github.com/users/zalando-incubator","html_url":"https://github.com/zalando-incubator","followers_url":"https://api.github.com/users/zalando-incubator/followers","following_url":"https://api.github.com/users/zalando-incubator/following{/other_user}","gists_url":"https://api.github.com/users/zalando-incubator/gists{/gist_id}","starred_url":"https://api.github.com/users/zalando-incubator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zalando-incubator/subscriptions","organizations_url":"https://api.github.com/users/zalando-incubator/orgs","repos_url":"https://api.github.com/users/zalando-incubator/repos","events_url":"https://api.github.com/users/zalando-incubator/events{/privacy}","received_events_url":"https://api.github.com/users/zalando-incubator/received_events","type":"Organization","site_admin":false},"repo":{"id":64120851,"name":"bubuku","full_name":"zalando-incubator/bubuku","owner":{"login":"zalando-incubator","id":18533786,"avatar_url":"https://avatars.githubusercontent.com/u/18533786?v=3","gravatar_id":"","url":"https://api.github.com/users/zalando-incubator","html_url":"https://github.com/zalando-incubator","followers_url":"https://api.github.com/users/zalando-incubator/followers","following_url":"https://api.github.com/users/zalando-incubator/following{/other_user}","gists_url":"https://api.github.com/users/zalando-incubator/gists{/gist_id}","starred_url":"https://api.github.com/users/zalando-incubator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zalando-incubator/subscriptions","organizations_url":"https://api.github.com/users/zalando-incubator/orgs","repos_url":"https://api.github.com/users/zalando-incubator/repos","events_url":"https://api.github.com/users/zalando-incubator/events{/privacy}","received_events_url":"https://api.github.com/users/zalando-incubator/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/zalando-incubator/bubuku","description":null,"fork":false,"url":"https://api.github.com/repos/zalando-incubator/bubuku","forks_url":"https://api.github.com/repos/zalando-incubator/bubuku/forks","keys_url":"https://api.github.com/repos/zalando-incubator/bubuku/keys{/key_id}","collaborators_url":"https://api.github.com/repos/zalando-incubator/bubuku/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/zalando-incubator/bubuku/teams","hooks_url":"https://api.github.com/repos/zalando-incubator/bubuku/hooks","issue_events_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/events{/number}","events_url":"https://api.github.com/repos/zalando-incubator/bubuku/events","assignees_url":"https://api.github.com/repos/zalando-incubator/bubuku/assignees{/user}","branches_url":"https://api.github.com/repos/zalando-incubator/bubuku/branches{/branch}","tags_url":"https://api.github.com/repos/zalando-incubator/bubuku/tags","blobs_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/refs{/sha}","trees_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/trees{/sha}","statuses_url":"https://api.github.com/repos/zalando-incubator/bubuku/statuses/{sha}","languages_url":"https://api.github.com/repos/zalando-incubator/bubuku/languages","stargazers_url":"https://api.github.com/repos/zalando-incubator/bubuku/stargazers","contributors_url":"https://api.github.com/repos/zalando-incubator/bubuku/contributors","subscribers_url":"https://api.github.com/repos/zalando-incubator/bubuku/subscribers","subscription_url":"https://api.github.com/repos/zalando-incubator/bubuku/subscription","commits_url":"https://api.github.com/repos/zalando-incubator/bubuku/commits{/sha}","git_commits_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/commits{/sha}","comments_url":"https://api.github.com/repos/zalando-incubator/bubuku/comments{/number}","issue_comment_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/comments{/number}","contents_url":"https://api.github.com/repos/zalando-incubator/bubuku/contents/{+path}","compare_url":"https://api.github.com/repos/zalando-incubator/bubuku/compare/{base}...{head}","merges_url":"https://api.github.com/repos/zalando-incubator/bubuku/merges","archive_url":"https://api.github.com/repos/zalando-incubator/bubuku/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/zalando-incubator/bubuku/downloads","issues_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues{/number}","pulls_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls{/number}","milestones_url":"https://api.github.com/repos/zalando-incubator/bubuku/milestones{/number}","notifications_url":"https://api.github.com/repos/zalando-incubator/bubuku/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/zalando-incubator/bubuku/labels{/name}","releases_url":"https://api.github.com/repos/zalando-incubator/bubuku/releases{/id}","deployments_url":"https://api.github.com/repos/zalando-incubator/bubuku/deployments","created_at":"2016-07-25T09:18:17Z","updated_at":"2016-09-05T09:24:26Z","pushed_at":"2016-09-23T07:25:15Z","git_url":"git://github.com/zalando-incubator/bubuku.git","ssh_url":"git@github.com:zalando-incubator/bubuku.git","clone_url":"https://github.com/zalando-incubator/bubuku.git","svn_url":"https://github.com/zalando-incubator/bubuku","homepage":null,"size":218,"stargazers_count":4,"watchers_count":4,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":4,"default_branch":"master"}},"base":{"label":"zalando-incubator:master","ref":"master","sha":"3b8c8f3e873b3fd86b5e6f748ffb73458307dbf2","user":{"login":"zalando-incubator","id":18533786,"avatar_url":"https://avatars.githubusercontent.com/u/18533786?v=3","gravatar_id":"","url":"https://api.github.com/users/zalando-incubator","html_url":"https://github.com/zalando-incubator","followers_url":"https://api.github.com/users/zalando-incubator/followers","following_url":"https://api.github.com/users/zalando-incubator/following{/other_user}","gists_url":"https://api.github.com/users/zalando-incubator/gists{/gist_id}","starred_url":"https://api.github.com/users/zalando-incubator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zalando-incubator/subscriptions","organizations_url":"https://api.github.com/users/zalando-incubator/orgs","repos_url":"https://api.github.com/users/zalando-incubator/repos","events_url":"https://api.github.com/users/zalando-incubator/events{/privacy}","received_events_url":"https://api.github.com/users/zalando-incubator/received_events","type":"Organization","site_admin":false},"repo":{"id":64120851,"name":"bubuku","full_name":"zalando-incubator/bubuku","owner":{"login":"zalando-incubator","id":18533786,"avatar_url":"https://avatars.githubusercontent.com/u/18533786?v=3","gravatar_id":"","url":"https://api.github.com/users/zalando-incubator","html_url":"https://github.com/zalando-incubator","followers_url":"https://api.github.com/users/zalando-incubator/followers","following_url":"https://api.github.com/users/zalando-incubator/following{/other_user}","gists_url":"https://api.github.com/users/zalando-incubator/gists{/gist_id}","starred_url":"https://api.github.com/users/zalando-incubator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zalando-incubator/subscriptions","organizations_url":"https://api.github.com/users/zalando-incubator/orgs","repos_url":"https://api.github.com/users/zalando-incubator/repos","events_url":"https://api.github.com/users/zalando-incubator/events{/privacy}","received_events_url":"https://api.github.com/users/zalando-incubator/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/zalando-incubator/bubuku","description":null,"fork":false,"url":"https://api.github.com/repos/zalando-incubator/bubuku","forks_url":"https://api.github.com/repos/zalando-incubator/bubuku/forks","keys_url":"https://api.github.com/repos/zalando-incubator/bubuku/keys{/key_id}","collaborators_url":"https://api.github.com/repos/zalando-incubator/bubuku/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/zalando-incubator/bubuku/teams","hooks_url":"https://api.github.com/repos/zalando-incubator/bubuku/hooks","issue_events_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/events{/number}","events_url":"https://api.github.com/repos/zalando-incubator/bubuku/events","assignees_url":"https://api.github.com/repos/zalando-incubator/bubuku/assignees{/user}","branches_url":"https://api.github.com/repos/zalando-incubator/bubuku/branches{/branch}","tags_url":"https://api.github.com/repos/zalando-incubator/bubuku/tags","blobs_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/refs{/sha}","trees_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/trees{/sha}","statuses_url":"https://api.github.com/repos/zalando-incubator/bubuku/statuses/{sha}","languages_url":"https://api.github.com/repos/zalando-incubator/bubuku/languages","stargazers_url":"https://api.github.com/repos/zalando-incubator/bubuku/stargazers","contributors_url":"https://api.github.com/repos/zalando-incubator/bubuku/contributors","subscribers_url":"https://api.github.com/repos/zalando-incubator/bubuku/subscribers","subscription_url":"https://api.github.com/repos/zalando-incubator/bubuku/subscription","commits_url":"https://api.github.com/repos/zalando-incubator/bubuku/commits{/sha}","git_commits_url":"https://api.github.com/repos/zalando-incubator/bubuku/git/commits{/sha}","comments_url":"https://api.github.com/repos/zalando-incubator/bubuku/comments{/number}","issue_comment_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues/comments{/number}","contents_url":"https://api.github.com/repos/zalando-incubator/bubuku/contents/{+path}","compare_url":"https://api.github.com/repos/zalando-incubator/bubuku/compare/{base}...{head}","merges_url":"https://api.github.com/repos/zalando-incubator/bubuku/merges","archive_url":"https://api.github.com/repos/zalando-incubator/bubuku/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/zalando-incubator/bubuku/downloads","issues_url":"https://api.github.com/repos/zalando-incubator/bubuku/issues{/number}","pulls_url":"https://api.github.com/repos/zalando-incubator/bubuku/pulls{/number}","milestones_url":"https://api.github.com/repos/zalando-incubator/bubuku/milestones{/number}","notifications_url":"https://api.github.com/repos/zalando-incubator/bubuku/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/zalando-incubator/bubuku/labels{/name}","releases_url":"https://api.github.com/repos/zalando-incubator/bubuku/releases{/id}","deployments_url":"https://api.github.com/repos/zalando-incubator/bubuku/deployments","created_at":"2016-07-25T09:18:17Z","updated_at":"2016-09-05T09:24:26Z","pushed_at":"2016-09-23T07:25:15Z","git_url":"git://github.com/zalando-incubator/bubuku.git","ssh_url":"git@github.com:zalando-incubator/bubuku.git","clone_url":"https://github.com/zalando-incubator/bubuku.git","svn_url":"https://github.com/zalando-incubator/bubuku","homepage":null,"size":218,"stargazers_count":4,"watchers_count":4,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":4,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66"},"html":{"href":"https://github.com/zalando-incubator/bubuku/pull/66"},"issue":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/issues/66"},"comments":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/issues/66/comments"},"review_comments":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66/comments"},"review_comment":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/pulls/66/commits"},"statuses":{"href":"https://api.github.com/repos/zalando-incubator/bubuku/statuses/f0cb48e35650e24d067f4952155d808b393ccd90"}}}}
{"actor":{"display_login":"rcillo"}}