{"action":"created","comment":{"url":"https://api.github.com/repos/confluentinc/ducktape/pulls/comments/71251769","id":71251769,"diff_hunk":"@@ -14,212 +14,175 @@\n \n \n import logging\n+import multiprocessing\n+import os\n import time\n-import traceback\n+import zmq\n \n-from ducktape.tests.result import TestResult, TestResults, IGNORE, PASS, FAIL\n-from ducktape.tests.reporter import SingleResultFileReporter\n-from ducktape.utils.local_filesystem_utils import mkdir_p\n+from ducktape.tests.serde import SerDe\n+from ducktape.tests.session import SessionLogger\n+from ducktape.command_line.defaults import ConsoleDefaults\n+from ducktape.tests.runner_client import run_client\n+from ducktape.tests.result import TestResults\n from ducktape.utils.terminal_size import get_terminal_size\n+from ducktape.tests.message import Request, ready_reply\n+from ducktape.tests.logger import Logger\n \n \n-class TestRunner(object):\n-    \"\"\"Abstract class responsible for running one or more tests.\"\"\"\n-    def __init__(self, session_context, tests):\n-        self.tests = tests\n+class RequestLogger(Logger):\n+    def __init__(self, session_context):\n         self.session_context = session_context\n-        self.results = TestResults(self.session_context)\n-\n-        self.logger.debug(\"Instantiating \" + self.who_am_i())\n \n     @property\n-    def cluster(self):\n-        return self.session_context.cluster\n+    def logger_name(self):\n+        # Naming means RequestLogger.logger is a child of SessionLogger.logger\n+        # I.e. requests\n+        return \"%s.requestlogger\" % SessionLogger(self.session_context).logger_name\n \n-    @property\n-    def logger(self):\n-        return self.session_context.logger\n+    def configure_logger(self):\n+        \"\"\"Log to request_log.\"\"\"\n+        if self.configured:\n+            return\n \n-    def who_am_i(self):\n-        \"\"\"Human-readable name helpful for logging.\"\"\"\n-        return self.__class__.__name__\n+        self._logger.setLevel(logging.DEBUG)\n \n-    def run_all_tests(self):\n-        raise NotImplementedError()\n+        fh_debug = logging.FileHandler(os.path.join(self.session_context.results_dir, \"request_log.debug\"))\n+        fh_debug.setLevel(logging.DEBUG)\n+\n+        # create formatter and add it to the handlers\n+        formatter = logging.Formatter(ConsoleDefaults.SESSION_LOG_FORMATTER)\n+        fh_debug.setFormatter(formatter)\n+\n+        # add the handlers to the logger\n+        self._logger.addHandler(fh_debug)\n \n \n-class SerialTestRunner(TestRunner):\n+class TestRunner(object):\n     \"\"\"Runs tests serially.\"\"\"\n \n     # When set to True, the test runner will finish running/cleaning the current test, but it will not run any more\n     stop_testing = False\n \n-    def __init__(self, *args, **kwargs):\n-        super(SerialTestRunner, self).__init__(*args, **kwargs)\n+    def __init__(self, cluster, session_context, session_logger, tests, port=ConsoleDefaults.TEST_DRIVER_PORT):\n+        # session_logger, message logger,\n+        self.session_logger = session_logger\n+        self.request_logger = RequestLogger(session_context).logger\n+        self.cluster = cluster\n+        self.serde = SerDe()\n+        self.port = port\n+\n+        self.zmq_context = zmq.Context()\n+        self.socket = self.zmq_context.socket(zmq.REP)\n+        self.socket.bind(\"tcp://*:%s\" % str(self.port))\n+        self.poller = zmq.Poller()\n+        self.poller.register(self.socket, zmq.POLLIN)\n+\n+        self.session_context = session_context\n+        self.results = TestResults(self.session_context)\n+\n+        self.proc_list = []\n+        self.staged_tests = tests\n+        self.active_tests = {}\n+        self.finished_tests = {}\n+\n         self.current_test = None\n         self.current_test_context = None\n \n-    def run_all_tests(self):\n+    def who_am_i(self):\n+        \"\"\"Human-readable name helpful for logging.\"\"\"\n+        return self.__class__.__name__\n \n+    def run_all_tests(self):\n         self.results.start_time = time.time()\n         self.log(logging.INFO, \"starting test run with session id %s...\" % self.session_context.session_id)\n-        self.log(logging.INFO, \"running %d tests...\" % len(self.tests))\n-\n-        for test_num, test_context in enumerate(self.tests, 1):\n-            if len(self.cluster) != self.cluster.num_available_nodes():\n-                # Sanity check - are we leaking cluster nodes?\n-                raise RuntimeError(\n-                    \"Expected all nodes to be available. Instead, %d of %d are available\" %\n-                    (self.cluster.num_available_nodes(), len(self.cluster)))\n-\n-            self.current_test_context = test_context\n-\n-            if self.current_test_context.ignore:\n-                # Skip running this test, but keep track of the fact that we ignored it\n-                result = TestResult(self.current_test_context,\n-                                    test_status=IGNORE, start_time=time.time(), stop_time=time.time())\n-                self.results.append(result)\n-                self.log(logging.INFO, \"Ignoring, and moving to next test...\")\n-                continue\n+        self.log(logging.INFO, \"running %d tests...\" % len(self.staged_tests))\n \n-            # Results from this test, as well as logs will be dumped here\n-            mkdir_p(self.current_test_context.results_dir)\n+        while len(self.staged_tests) > 0 or len(self.active_tests) > 0:\n \n-            start_time = -1\n-            stop_time = -1\n-            test_status = PASS\n-            summary = \"\"\n-            data = None\n+            if len(self.active_tests) == 0:\n+                self.current_test_context = self.staged_tests.pop()\n+                self.run_single_test()\n \n             try:\n-                # Instantiate test\n-                self.current_test = test_context.cls(test_context)\n-\n-                # Run the test unit\n-                start_time = time.time()\n-                self.log(logging.INFO, \"test %d of %d\" % (test_num, len(self.tests)))\n-\n-                self.log(logging.INFO, \"setting up\")\n-                self.setup_single_test()\n-\n-                self.log(logging.INFO, \"running\")\n-                data = self.run_single_test()\n-                test_status = PASS\n-                self.log(logging.INFO, \"PASS\")\n-\n-            except BaseException as e:\n-                err_trace = str(e.message) + \"\\n\" + traceback.format_exc(limit=16)\n-                self.log(logging.INFO, \"FAIL: \" + err_trace)\n-\n-                test_status = FAIL\n-                summary += err_trace\n-\n-                self.stop_testing = self.session_context.exit_first or isinstance(e, KeyboardInterrupt)\n-\n-            finally:\n-                self.teardown_single_test(teardown_services=not self.session_context.no_teardown)\n+                message = self.socket.recv()\n+                event = self.serde.deserialize(message)\n+                self.handle(event)\n+            except Exception as e:\n+                s = str(type(e))\n+                s += \" \" + str(e)\n+                print \"Exception receiving message:\", str(e)\n+                continue\n \n-                stop_time = time.time()\n+        for proc in self.proc_list:\n+            proc.join()\n \n-                result = TestResult(self.current_test_context, test_status, summary, data, start_time, stop_time)\n-                self.results.append(result)\n+        return self.results\n \n-                self.log(logging.INFO, \"Summary: %s\" % str(result.summary))\n-                self.log(logging.INFO, \"Data: %s\" % str(result.data))\n-                if test_num < len(self.tests):\n-                    terminal_width, y = get_terminal_size()\n-                    print \"~\" * int(2 * terminal_width / 3)\n+    def run_single_test(self):\n+        proc = multiprocessing.Process(\n+            target=run_client,\n+            args=[\n+                self.port,\n+                self.current_test_context.logger_name,\n+                self.current_test_context.results_dir,\n+                self.session_context.debug,\n+                self.session_context.max_parallel\n+            ])\n+        self.proc_list.append(proc)\n+        proc.start()\n+\n+    def handle(self, event):\n+        self.request_logger.debug(event)\n+\n+        if event[\"event_type\"] == Request.READY:\n+            self.handle_ready(event)\n+        elif event[\"event_type\"] in [Request.RUNNING, Request.SETTING_UP, Request.TEARING_DOWN]:\n+            self.handle_lifecycle(event)\n+        elif event[\"event_type\"] == Request.FINISHED:\n+            self.handle_finished(event)\n+        elif event[\"event_type\"] == Request.LOG:\n+            self.handle_log(event)\n+        else:\n+            self.handle_other(event)","path":"ducktape/tests/runner.py","position":217,"original_position":217,"commit_id":"5b1151d178889872a6eb407eda60332df9d776c6","original_commit_id":"5b1151d178889872a6eb407eda60332df9d776c6","user":{"login":"ewencp","id":47366,"avatar_url":"https://avatars.githubusercontent.com/u/47366?v=3","gravatar_id":"","url":"https://api.github.com/users/ewencp","html_url":"https://github.com/ewencp","followers_url":"https://api.github.com/users/ewencp/followers","following_url":"https://api.github.com/users/ewencp/following{/other_user}","gists_url":"https://api.github.com/users/ewencp/gists{/gist_id}","starred_url":"https://api.github.com/users/ewencp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ewencp/subscriptions","organizations_url":"https://api.github.com/users/ewencp/orgs","repos_url":"https://api.github.com/users/ewencp/repos","events_url":"https://api.github.com/users/ewencp/events{/privacy}","received_events_url":"https://api.github.com/users/ewencp/received_events","type":"User","site_admin":false},"body":"Shouldn't this just be an error since it is unexpected?","created_at":"2016-07-18T23:50:06Z","updated_at":"2016-07-18T23:50:06Z","html_url":"https://github.com/confluentinc/ducktape/pull/146#discussion_r71251769","pull_request_url":"https://api.github.com/repos/confluentinc/ducktape/pulls/146","_links":{"self":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/comments/71251769"},"html":{"href":"https://github.com/confluentinc/ducktape/pull/146#discussion_r71251769"},"pull_request":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/146"}}},"pull_request":{"url":"https://api.github.com/repos/confluentinc/ducktape/pulls/146","id":77382051,"html_url":"https://github.com/confluentinc/ducktape/pull/146","diff_url":"https://github.com/confluentinc/ducktape/pull/146.diff","patch_url":"https://github.com/confluentinc/ducktape/pull/146.patch","issue_url":"https://api.github.com/repos/confluentinc/ducktape/issues/146","number":146,"state":"open","locked":false,"title":"[WIP] Spawn subprocess","user":{"login":"granders","id":1295932,"avatar_url":"https://avatars.githubusercontent.com/u/1295932?v=3","gravatar_id":"","url":"https://api.github.com/users/granders","html_url":"https://github.com/granders","followers_url":"https://api.github.com/users/granders/followers","following_url":"https://api.github.com/users/granders/following{/other_user}","gists_url":"https://api.github.com/users/granders/gists{/gist_id}","starred_url":"https://api.github.com/users/granders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/granders/subscriptions","organizations_url":"https://api.github.com/users/granders/orgs","repos_url":"https://api.github.com/users/granders/repos","events_url":"https://api.github.com/users/granders/events{/privacy}","received_events_url":"https://api.github.com/users/granders/received_events","type":"User","site_admin":false},"body":"PR into parallel-test-runner branch since I plan to do a few more incremental pull requests before merging parallel-test-runner into master.\r\n\r\nTests still run sequentially, but this PR updates ducktape to run tests in subprocesses, using message passing to coordinate starting of new tests.\r\n","created_at":"2016-07-14T00:09:12Z","updated_at":"2016-07-18T23:50:06Z","closed_at":null,"merged_at":null,"merge_commit_sha":"00e7e4016c6abb1f445f8b85f68b5e7692cff561","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/confluentinc/ducktape/pulls/146/commits","review_comments_url":"https://api.github.com/repos/confluentinc/ducktape/pulls/146/comments","review_comment_url":"https://api.github.com/repos/confluentinc/ducktape/pulls/comments{/number}","comments_url":"https://api.github.com/repos/confluentinc/ducktape/issues/146/comments","statuses_url":"https://api.github.com/repos/confluentinc/ducktape/statuses/5b1151d178889872a6eb407eda60332df9d776c6","head":{"label":"confluentinc:spawn-subprocess-rebase","ref":"spawn-subprocess-rebase","sha":"5b1151d178889872a6eb407eda60332df9d776c6","user":{"login":"confluentinc","id":9439498,"avatar_url":"https://avatars.githubusercontent.com/u/9439498?v=3","gravatar_id":"","url":"https://api.github.com/users/confluentinc","html_url":"https://github.com/confluentinc","followers_url":"https://api.github.com/users/confluentinc/followers","following_url":"https://api.github.com/users/confluentinc/following{/other_user}","gists_url":"https://api.github.com/users/confluentinc/gists{/gist_id}","starred_url":"https://api.github.com/users/confluentinc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/confluentinc/subscriptions","organizations_url":"https://api.github.com/users/confluentinc/orgs","repos_url":"https://api.github.com/users/confluentinc/repos","events_url":"https://api.github.com/users/confluentinc/events{/privacy}","received_events_url":"https://api.github.com/users/confluentinc/received_events","type":"Organization","site_admin":false},"repo":{"id":28360638,"name":"ducktape","full_name":"confluentinc/ducktape","owner":{"login":"confluentinc","id":9439498,"avatar_url":"https://avatars.githubusercontent.com/u/9439498?v=3","gravatar_id":"","url":"https://api.github.com/users/confluentinc","html_url":"https://github.com/confluentinc","followers_url":"https://api.github.com/users/confluentinc/followers","following_url":"https://api.github.com/users/confluentinc/following{/other_user}","gists_url":"https://api.github.com/users/confluentinc/gists{/gist_id}","starred_url":"https://api.github.com/users/confluentinc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/confluentinc/subscriptions","organizations_url":"https://api.github.com/users/confluentinc/orgs","repos_url":"https://api.github.com/users/confluentinc/repos","events_url":"https://api.github.com/users/confluentinc/events{/privacy}","received_events_url":"https://api.github.com/users/confluentinc/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/confluentinc/ducktape","description":"System integration and performance tests","fork":false,"url":"https://api.github.com/repos/confluentinc/ducktape","forks_url":"https://api.github.com/repos/confluentinc/ducktape/forks","keys_url":"https://api.github.com/repos/confluentinc/ducktape/keys{/key_id}","collaborators_url":"https://api.github.com/repos/confluentinc/ducktape/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/confluentinc/ducktape/teams","hooks_url":"https://api.github.com/repos/confluentinc/ducktape/hooks","issue_events_url":"https://api.github.com/repos/confluentinc/ducktape/issues/events{/number}","events_url":"https://api.github.com/repos/confluentinc/ducktape/events","assignees_url":"https://api.github.com/repos/confluentinc/ducktape/assignees{/user}","branches_url":"https://api.github.com/repos/confluentinc/ducktape/branches{/branch}","tags_url":"https://api.github.com/repos/confluentinc/ducktape/tags","blobs_url":"https://api.github.com/repos/confluentinc/ducktape/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/confluentinc/ducktape/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/confluentinc/ducktape/git/refs{/sha}","trees_url":"https://api.github.com/repos/confluentinc/ducktape/git/trees{/sha}","statuses_url":"https://api.github.com/repos/confluentinc/ducktape/statuses/{sha}","languages_url":"https://api.github.com/repos/confluentinc/ducktape/languages","stargazers_url":"https://api.github.com/repos/confluentinc/ducktape/stargazers","contributors_url":"https://api.github.com/repos/confluentinc/ducktape/contributors","subscribers_url":"https://api.github.com/repos/confluentinc/ducktape/subscribers","subscription_url":"https://api.github.com/repos/confluentinc/ducktape/subscription","commits_url":"https://api.github.com/repos/confluentinc/ducktape/commits{/sha}","git_commits_url":"https://api.github.com/repos/confluentinc/ducktape/git/commits{/sha}","comments_url":"https://api.github.com/repos/confluentinc/ducktape/comments{/number}","issue_comment_url":"https://api.github.com/repos/confluentinc/ducktape/issues/comments{/number}","contents_url":"https://api.github.com/repos/confluentinc/ducktape/contents/{+path}","compare_url":"https://api.github.com/repos/confluentinc/ducktape/compare/{base}...{head}","merges_url":"https://api.github.com/repos/confluentinc/ducktape/merges","archive_url":"https://api.github.com/repos/confluentinc/ducktape/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/confluentinc/ducktape/downloads","issues_url":"https://api.github.com/repos/confluentinc/ducktape/issues{/number}","pulls_url":"https://api.github.com/repos/confluentinc/ducktape/pulls{/number}","milestones_url":"https://api.github.com/repos/confluentinc/ducktape/milestones{/number}","notifications_url":"https://api.github.com/repos/confluentinc/ducktape/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/confluentinc/ducktape/labels{/name}","releases_url":"https://api.github.com/repos/confluentinc/ducktape/releases{/id}","deployments_url":"https://api.github.com/repos/confluentinc/ducktape/deployments","created_at":"2014-12-22T21:38:32Z","updated_at":"2016-07-12T15:42:56Z","pushed_at":"2016-07-16T22:34:56Z","git_url":"git://github.com/confluentinc/ducktape.git","ssh_url":"git@github.com:confluentinc/ducktape.git","clone_url":"https://github.com/confluentinc/ducktape.git","svn_url":"https://github.com/confluentinc/ducktape","homepage":null,"size":617,"stargazers_count":33,"watchers_count":33,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":37,"forks":10,"open_issues":37,"watchers":33,"default_branch":"master"}},"base":{"label":"confluentinc:parallel-test-runner","ref":"parallel-test-runner","sha":"c375ad36162de25bf43c33d1d2199d0170cda8d7","user":{"login":"confluentinc","id":9439498,"avatar_url":"https://avatars.githubusercontent.com/u/9439498?v=3","gravatar_id":"","url":"https://api.github.com/users/confluentinc","html_url":"https://github.com/confluentinc","followers_url":"https://api.github.com/users/confluentinc/followers","following_url":"https://api.github.com/users/confluentinc/following{/other_user}","gists_url":"https://api.github.com/users/confluentinc/gists{/gist_id}","starred_url":"https://api.github.com/users/confluentinc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/confluentinc/subscriptions","organizations_url":"https://api.github.com/users/confluentinc/orgs","repos_url":"https://api.github.com/users/confluentinc/repos","events_url":"https://api.github.com/users/confluentinc/events{/privacy}","received_events_url":"https://api.github.com/users/confluentinc/received_events","type":"Organization","site_admin":false},"repo":{"id":28360638,"name":"ducktape","full_name":"confluentinc/ducktape","owner":{"login":"confluentinc","id":9439498,"avatar_url":"https://avatars.githubusercontent.com/u/9439498?v=3","gravatar_id":"","url":"https://api.github.com/users/confluentinc","html_url":"https://github.com/confluentinc","followers_url":"https://api.github.com/users/confluentinc/followers","following_url":"https://api.github.com/users/confluentinc/following{/other_user}","gists_url":"https://api.github.com/users/confluentinc/gists{/gist_id}","starred_url":"https://api.github.com/users/confluentinc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/confluentinc/subscriptions","organizations_url":"https://api.github.com/users/confluentinc/orgs","repos_url":"https://api.github.com/users/confluentinc/repos","events_url":"https://api.github.com/users/confluentinc/events{/privacy}","received_events_url":"https://api.github.com/users/confluentinc/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/confluentinc/ducktape","description":"System integration and performance tests","fork":false,"url":"https://api.github.com/repos/confluentinc/ducktape","forks_url":"https://api.github.com/repos/confluentinc/ducktape/forks","keys_url":"https://api.github.com/repos/confluentinc/ducktape/keys{/key_id}","collaborators_url":"https://api.github.com/repos/confluentinc/ducktape/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/confluentinc/ducktape/teams","hooks_url":"https://api.github.com/repos/confluentinc/ducktape/hooks","issue_events_url":"https://api.github.com/repos/confluentinc/ducktape/issues/events{/number}","events_url":"https://api.github.com/repos/confluentinc/ducktape/events","assignees_url":"https://api.github.com/repos/confluentinc/ducktape/assignees{/user}","branches_url":"https://api.github.com/repos/confluentinc/ducktape/branches{/branch}","tags_url":"https://api.github.com/repos/confluentinc/ducktape/tags","blobs_url":"https://api.github.com/repos/confluentinc/ducktape/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/confluentinc/ducktape/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/confluentinc/ducktape/git/refs{/sha}","trees_url":"https://api.github.com/repos/confluentinc/ducktape/git/trees{/sha}","statuses_url":"https://api.github.com/repos/confluentinc/ducktape/statuses/{sha}","languages_url":"https://api.github.com/repos/confluentinc/ducktape/languages","stargazers_url":"https://api.github.com/repos/confluentinc/ducktape/stargazers","contributors_url":"https://api.github.com/repos/confluentinc/ducktape/contributors","subscribers_url":"https://api.github.com/repos/confluentinc/ducktape/subscribers","subscription_url":"https://api.github.com/repos/confluentinc/ducktape/subscription","commits_url":"https://api.github.com/repos/confluentinc/ducktape/commits{/sha}","git_commits_url":"https://api.github.com/repos/confluentinc/ducktape/git/commits{/sha}","comments_url":"https://api.github.com/repos/confluentinc/ducktape/comments{/number}","issue_comment_url":"https://api.github.com/repos/confluentinc/ducktape/issues/comments{/number}","contents_url":"https://api.github.com/repos/confluentinc/ducktape/contents/{+path}","compare_url":"https://api.github.com/repos/confluentinc/ducktape/compare/{base}...{head}","merges_url":"https://api.github.com/repos/confluentinc/ducktape/merges","archive_url":"https://api.github.com/repos/confluentinc/ducktape/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/confluentinc/ducktape/downloads","issues_url":"https://api.github.com/repos/confluentinc/ducktape/issues{/number}","pulls_url":"https://api.github.com/repos/confluentinc/ducktape/pulls{/number}","milestones_url":"https://api.github.com/repos/confluentinc/ducktape/milestones{/number}","notifications_url":"https://api.github.com/repos/confluentinc/ducktape/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/confluentinc/ducktape/labels{/name}","releases_url":"https://api.github.com/repos/confluentinc/ducktape/releases{/id}","deployments_url":"https://api.github.com/repos/confluentinc/ducktape/deployments","created_at":"2014-12-22T21:38:32Z","updated_at":"2016-07-12T15:42:56Z","pushed_at":"2016-07-16T22:34:56Z","git_url":"git://github.com/confluentinc/ducktape.git","ssh_url":"git@github.com:confluentinc/ducktape.git","clone_url":"https://github.com/confluentinc/ducktape.git","svn_url":"https://github.com/confluentinc/ducktape","homepage":null,"size":617,"stargazers_count":33,"watchers_count":33,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":10,"mirror_url":null,"open_issues_count":37,"forks":10,"open_issues":37,"watchers":33,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/146"},"html":{"href":"https://github.com/confluentinc/ducktape/pull/146"},"issue":{"href":"https://api.github.com/repos/confluentinc/ducktape/issues/146"},"comments":{"href":"https://api.github.com/repos/confluentinc/ducktape/issues/146/comments"},"review_comments":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/146/comments"},"review_comment":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/confluentinc/ducktape/pulls/146/commits"},"statuses":{"href":"https://api.github.com/repos/confluentinc/ducktape/statuses/5b1151d178889872a6eb407eda60332df9d776c6"}}}}
{"actor":{"display_login":"ewencp"}}