{"action":"created","comment":{"url":"https://api.github.com/repos/ray-project/ray/pulls/comments/206011195","pull_request_review_id":141387693,"id":206011195,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwNjAxMTE5NQ==","diff_hunk":"@@ -2,33 +2,131 @@\n #define RAY_RAYLET_RECONSTRUCTION_POLICY_H\n \n #include <functional>\n+#include <unordered_map>\n+#include <unordered_set>\n \n+#include <boost/asio.hpp>\n+\n+#include \"ray/gcs/tables.h\"\n #include \"ray/id.h\"\n+#include \"ray/util/util.h\"\n+\n+#include \"ray/object_manager/object_directory.h\"\n \n namespace ray {\n \n namespace raylet {\n \n-// TODO(swang): Use std::function instead of boost.\n+class ReconstructionPolicyInterface {\n+ public:\n+  virtual void Listen(const ObjectID &object_id) = 0;\n+  virtual void Cancel(const ObjectID &object_id) = 0;\n+  virtual ~ReconstructionPolicyInterface(){};\n+};\n \n-class ReconstructionPolicy {\n+class ReconstructionPolicy : public ReconstructionPolicyInterface {\n  public:\n   /// Create the reconstruction policy.\n   ///\n+  /// \\param io_service The event loop to attach reconstruction timers to.\n   /// \\param reconstruction_handler The handler to call if a task needs to be\n   /// re-executed.\n-  // TODO(swang): This requires at minimum references to the Raylet's lineage\n-  // cache and GCS client.\n-  ReconstructionPolicy(std::function<void(const TaskID &)> reconstruction_handler) {}\n+  /// \\param initial_reconstruction_timeout_ms The initial timeout within which\n+  /// a task lease notification must be received. Otherwise, reconstruction\n+  /// will be triggered.\n+  /// \\param client_id The client ID to use when requesting notifications from\n+  /// the GCS.\n+  /// \\param task_lease_pubsub The GCS pub-sub storage system to request task\n+  /// lease notifications from.\n+  ReconstructionPolicy(boost::asio::io_service &io_service,\n+                       std::function<void(const TaskID &)> reconstruction_handler,\n+                       int64_t initial_reconstruction_timeout_ms,\n+                       const ClientID &client_id,\n+                       gcs::PubsubInterface<TaskID> &task_lease_pubsub,\n+                       std::shared_ptr<ObjectDirectoryInterface> object_directory);\n \n-  /// Check whether an object requires reconstruction. If this object requires\n-  /// reconstruction, the registered task reconstruction handler will be called\n-  /// for each task that needs to be re-executed.\n+  /// Listen for task lease notifications about an object that may require\n+  /// reconstruction. If no notifications are received within the initial\n+  /// timeout, then the registered task reconstruction handler will be called\n+  /// for the task that created the object.\n   ///\n   /// \\param object_id The object to check for reconstruction.\n-  void CheckObjectReconstruction(const ObjectID &object_id);\n+  void Listen(const ObjectID &object_id);\n+\n+  /// Cancel listening for an object. Notifications for the object will be\n+  /// ignored. This does not cancel a reconstruction attempt that is already in\n+  /// progress.\n+  ///\n+  /// \\param object_id The object to cancel.\n+  void Cancel(const ObjectID &object_id);\n+\n+  /// Handle a notification for a task lease. This handler should be called to\n+  /// indicate that a task is currently being executed, so any objects that it\n+  /// creates should not be reconstructed.\n+  ///\n+  /// \\param task_id The task ID of the task being executed.\n+  /// \\param lease_timeout_ms After this timeout, the task's lease is\n+  /// guaranteed to be expired. If a second notification is not received within\n+  /// this timeout, then objects that the task creates may be reconstructed.\n+  void HandleTaskLeaseNotification(const TaskID &task_id, int64_t lease_timeout_ms);\n \n  private:\n+  struct ReconstructionTask {\n+    ReconstructionTask(boost::asio::io_service &io_service)\n+        : expires_at(INT64_MAX),\n+          subscribed(false),\n+          reconstruction_attempt(0),\n+          reconstruction_timer(new boost::asio::deadline_timer(io_service)) {}\n+\n+    // The objects created by this task that we are listening for notifications for.\n+    std::unordered_set<ObjectID> created_objects;\n+    // The time at which the timer for this task expires, according to this\n+    // node's steady clock.\n+    int64_t expires_at;\n+    // Whether we are subscribed to lease notifications for this task.\n+    bool subscribed;\n+    // The number of times we've attempted reconstructing this task so far.\n+    int reconstruction_attempt;\n+    // The task's reconstruction timer. If this expires before a lease\n+    // notification is received, then the task will be reconstructed.\n+    std::unique_ptr<boost::asio::deadline_timer> reconstruction_timer;\n+  };\n+\n+  // Set the reconstruction timer for a task. If no task lease notifications\n+  // are received within the timeout, then reconstruction will be triggered.\n+  void SetTaskTimeout(std::unordered_map<TaskID, ReconstructionTask>::iterator task_it,\n+                      int64_t timeout_ms);\n+\n+  /// Attempt to re-execute a task to reconstruct the required object.\n+  ///\n+  /// \\param task_id The task to attempt to re-execute.\n+  /// \\param required_object_id The object created by the task that requires\n+  /// reconstruction.\n+  /// \\param reconstruction_attempt What number attempt this is at\n+  /// reconstructing the task. This is used to suppress duplicate\n+  /// reconstructions of the same task (e.g., if a task creates two objects\n+  /// that both require reconstruction).\n+  void AttemptReconstruction(const TaskID &task_id, const ObjectID &required_object_id,\n+                             int reconstruction_attempt);\n+\n+  /// Handle expiration of a task lease.\n+  void HandleTaskLeaseExpired(const TaskID &task_id);\n+\n+  /// The event loop.\n+  boost::asio::io_service &io_service_;\n+  /// The handler to call for tasks that require reconstruction.\n+  const std::function<void(const TaskID &)> reconstruction_handler_;\n+  /// The initial timeout within which a task lease notification must be\n+  /// received. Otherwise, reconstruction will be triggered.\n+  const int64_t initial_reconstruction_timeout_ms_;\n+  /// The client ID to use when requesting notifications from the GCS.\n+  const ClientID client_id_;\n+  /// The GCS pub-sub storage system to request task lease notifications from.\n+  gcs::PubsubInterface<TaskID> &task_lease_pubsub_;\n+  /// The object directory used to lookup object locations.\n+  std::shared_ptr<ObjectDirectoryInterface> object_directory_;","path":"src/ray/raylet/reconstruction_policy.h","position":135,"original_position":135,"commit_id":"5b27890d8165129d92018fe3a83a40b02db3fedf","original_commit_id":"5b27890d8165129d92018fe3a83a40b02db3fedf","user":{"login":"stephanie-wang","id":2560516,"node_id":"MDQ6VXNlcjI1NjA1MTY=","avatar_url":"https://avatars2.githubusercontent.com/u/2560516?v=4","gravatar_id":"","url":"https://api.github.com/users/stephanie-wang","html_url":"https://github.com/stephanie-wang","followers_url":"https://api.github.com/users/stephanie-wang/followers","following_url":"https://api.github.com/users/stephanie-wang/following{/other_user}","gists_url":"https://api.github.com/users/stephanie-wang/gists{/gist_id}","starred_url":"https://api.github.com/users/stephanie-wang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephanie-wang/subscriptions","organizations_url":"https://api.github.com/users/stephanie-wang/orgs","repos_url":"https://api.github.com/users/stephanie-wang/repos","events_url":"https://api.github.com/users/stephanie-wang/events{/privacy}","received_events_url":"https://api.github.com/users/stephanie-wang/received_events","type":"User","site_admin":false},"body":"Hmm I guess this isn't the best reason, but I have it as a `shared_ptr` partly to make testing easier for the `ReconstructionPolicy`.\r\n\r\nBut I also think in the future, it should really just be a single `shared_ptr<ObjectDirectoryInterface>` that's used by all components. I tried initially to see if I could share the one used by the `ObjectManager` with the `ReconstructionPolicy`, but it seemed easier to just separate them for now.","created_at":"2018-07-30T04:11:14Z","updated_at":"2018-07-30T04:11:15Z","html_url":"https://github.com/ray-project/ray/pull/2497#discussion_r206011195","pull_request_url":"https://api.github.com/repos/ray-project/ray/pulls/2497","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/ray-project/ray/pulls/comments/206011195"},"html":{"href":"https://github.com/ray-project/ray/pull/2497#discussion_r206011195"},"pull_request":{"href":"https://api.github.com/repos/ray-project/ray/pulls/2497"}},"in_reply_to_id":205987164},"pull_request":{"url":"https://api.github.com/repos/ray-project/ray/pulls/2497","id":204528001,"node_id":"MDExOlB1bGxSZXF1ZXN0MjA0NTI4MDAx","html_url":"https://github.com/ray-project/ray/pull/2497","diff_url":"https://github.com/ray-project/ray/pull/2497.diff","patch_url":"https://github.com/ray-project/ray/pull/2497.patch","issue_url":"https://api.github.com/repos/ray-project/ray/issues/2497","number":2497,"state":"open","locked":false,"title":"[xray] Implement task lease table, logic for deciding when to reconstruct a task","user":{"login":"stephanie-wang","id":2560516,"node_id":"MDQ6VXNlcjI1NjA1MTY=","avatar_url":"https://avatars2.githubusercontent.com/u/2560516?v=4","gravatar_id":"","url":"https://api.github.com/users/stephanie-wang","html_url":"https://github.com/stephanie-wang","followers_url":"https://api.github.com/users/stephanie-wang/followers","following_url":"https://api.github.com/users/stephanie-wang/following{/other_user}","gists_url":"https://api.github.com/users/stephanie-wang/gists{/gist_id}","starred_url":"https://api.github.com/users/stephanie-wang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephanie-wang/subscriptions","organizations_url":"https://api.github.com/users/stephanie-wang/orgs","repos_url":"https://api.github.com/users/stephanie-wang/repos","events_url":"https://api.github.com/users/stephanie-wang/events{/privacy}","received_events_url":"https://api.github.com/users/stephanie-wang/received_events","type":"User","site_admin":false},"body":"## What do these changes do?\r\n\r\nThis implements the logic for deciding when reconstruction for a task is necessary.\r\n\r\nThe core of the algorithm is a \"task lease table\". This is a table in the GCS that lists which node managers are currently responsible for executing which tasks, except that each entry has an expiration date. When a node manager has a task queued or executing, it must \"acquire the lease\" by adding an entry to the task lease table. It also periodically renews the lease by overwriting the entry before its expiration time passes.\r\n\r\nThe `ReconstructionPolicy` uses notifications from this table to determine when reconstruction for a task is necessary. The basic algorithm is:\r\n1. Wait for a configured initial period for a notification from the lease table.\r\n2. If no notification arrives or the current lease in the table is expired, then look up the objects created by the task that are needed by this node. If at least one of these objects no longer exists on a live node, then submit the task for reconstruction.\r\n3. Go back to 1.\r\n\r\nNote that right now no one cleans up the task lease table. Two potential options are: (1) have the node that executes the task clean up the corresponding entry, or (2) flush the task lease table using credis flushing. If we go with option (2), we must be careful not to flush a task lease entry that is still active.","created_at":"2018-07-27T20:07:13Z","updated_at":"2018-07-30T04:11:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c6578081cf1442c8f678667ed1b29367aa766bca","assignee":null,"assignees":[],"requested_reviewers":[{"login":"pcmoritz","id":113316,"node_id":"MDQ6VXNlcjExMzMxNg==","avatar_url":"https://avatars1.githubusercontent.com/u/113316?v=4","gravatar_id":"","url":"https://api.github.com/users/pcmoritz","html_url":"https://github.com/pcmoritz","followers_url":"https://api.github.com/users/pcmoritz/followers","following_url":"https://api.github.com/users/pcmoritz/following{/other_user}","gists_url":"https://api.github.com/users/pcmoritz/gists{/gist_id}","starred_url":"https://api.github.com/users/pcmoritz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pcmoritz/subscriptions","organizations_url":"https://api.github.com/users/pcmoritz/orgs","repos_url":"https://api.github.com/users/pcmoritz/repos","events_url":"https://api.github.com/users/pcmoritz/events{/privacy}","received_events_url":"https://api.github.com/users/pcmoritz/received_events","type":"User","site_admin":false},{"login":"atumanov","id":1316372,"node_id":"MDQ6VXNlcjEzMTYzNzI=","avatar_url":"https://avatars1.githubusercontent.com/u/1316372?v=4","gravatar_id":"","url":"https://api.github.com/users/atumanov","html_url":"https://github.com/atumanov","followers_url":"https://api.github.com/users/atumanov/followers","following_url":"https://api.github.com/users/atumanov/following{/other_user}","gists_url":"https://api.github.com/users/atumanov/gists{/gist_id}","starred_url":"https://api.github.com/users/atumanov/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/atumanov/subscriptions","organizations_url":"https://api.github.com/users/atumanov/orgs","repos_url":"https://api.github.com/users/atumanov/repos","events_url":"https://api.github.com/users/atumanov/events{/privacy}","received_events_url":"https://api.github.com/users/atumanov/received_events","type":"User","site_admin":false},{"login":"elibol","id":1884921,"node_id":"MDQ6VXNlcjE4ODQ5MjE=","avatar_url":"https://avatars1.githubusercontent.com/u/1884921?v=4","gravatar_id":"","url":"https://api.github.com/users/elibol","html_url":"https://github.com/elibol","followers_url":"https://api.github.com/users/elibol/followers","following_url":"https://api.github.com/users/elibol/following{/other_user}","gists_url":"https://api.github.com/users/elibol/gists{/gist_id}","starred_url":"https://api.github.com/users/elibol/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/elibol/subscriptions","organizations_url":"https://api.github.com/users/elibol/orgs","repos_url":"https://api.github.com/users/elibol/repos","events_url":"https://api.github.com/users/elibol/events{/privacy}","received_events_url":"https://api.github.com/users/elibol/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/ray-project/ray/pulls/2497/commits","review_comments_url":"https://api.github.com/repos/ray-project/ray/pulls/2497/comments","review_comment_url":"https://api.github.com/repos/ray-project/ray/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ray-project/ray/issues/2497/comments","statuses_url":"https://api.github.com/repos/ray-project/ray/statuses/5b27890d8165129d92018fe3a83a40b02db3fedf","head":{"label":"stephanie-wang:task-leases","ref":"task-leases","sha":"5b27890d8165129d92018fe3a83a40b02db3fedf","user":{"login":"stephanie-wang","id":2560516,"node_id":"MDQ6VXNlcjI1NjA1MTY=","avatar_url":"https://avatars2.githubusercontent.com/u/2560516?v=4","gravatar_id":"","url":"https://api.github.com/users/stephanie-wang","html_url":"https://github.com/stephanie-wang","followers_url":"https://api.github.com/users/stephanie-wang/followers","following_url":"https://api.github.com/users/stephanie-wang/following{/other_user}","gists_url":"https://api.github.com/users/stephanie-wang/gists{/gist_id}","starred_url":"https://api.github.com/users/stephanie-wang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephanie-wang/subscriptions","organizations_url":"https://api.github.com/users/stephanie-wang/orgs","repos_url":"https://api.github.com/users/stephanie-wang/repos","events_url":"https://api.github.com/users/stephanie-wang/events{/privacy}","received_events_url":"https://api.github.com/users/stephanie-wang/received_events","type":"User","site_admin":false},"repo":{"id":77302388,"node_id":"MDEwOlJlcG9zaXRvcnk3NzMwMjM4OA==","name":"ray","full_name":"stephanie-wang/ray","owner":{"login":"stephanie-wang","id":2560516,"node_id":"MDQ6VXNlcjI1NjA1MTY=","avatar_url":"https://avatars2.githubusercontent.com/u/2560516?v=4","gravatar_id":"","url":"https://api.github.com/users/stephanie-wang","html_url":"https://github.com/stephanie-wang","followers_url":"https://api.github.com/users/stephanie-wang/followers","following_url":"https://api.github.com/users/stephanie-wang/following{/other_user}","gists_url":"https://api.github.com/users/stephanie-wang/gists{/gist_id}","starred_url":"https://api.github.com/users/stephanie-wang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephanie-wang/subscriptions","organizations_url":"https://api.github.com/users/stephanie-wang/orgs","repos_url":"https://api.github.com/users/stephanie-wang/repos","events_url":"https://api.github.com/users/stephanie-wang/events{/privacy}","received_events_url":"https://api.github.com/users/stephanie-wang/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/stephanie-wang/ray","description":"An experimental distributed execution engine","fork":true,"url":"https://api.github.com/repos/stephanie-wang/ray","forks_url":"https://api.github.com/repos/stephanie-wang/ray/forks","keys_url":"https://api.github.com/repos/stephanie-wang/ray/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stephanie-wang/ray/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stephanie-wang/ray/teams","hooks_url":"https://api.github.com/repos/stephanie-wang/ray/hooks","issue_events_url":"https://api.github.com/repos/stephanie-wang/ray/issues/events{/number}","events_url":"https://api.github.com/repos/stephanie-wang/ray/events","assignees_url":"https://api.github.com/repos/stephanie-wang/ray/assignees{/user}","branches_url":"https://api.github.com/repos/stephanie-wang/ray/branches{/branch}","tags_url":"https://api.github.com/repos/stephanie-wang/ray/tags","blobs_url":"https://api.github.com/repos/stephanie-wang/ray/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stephanie-wang/ray/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stephanie-wang/ray/git/refs{/sha}","trees_url":"https://api.github.com/repos/stephanie-wang/ray/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stephanie-wang/ray/statuses/{sha}","languages_url":"https://api.github.com/repos/stephanie-wang/ray/languages","stargazers_url":"https://api.github.com/repos/stephanie-wang/ray/stargazers","contributors_url":"https://api.github.com/repos/stephanie-wang/ray/contributors","subscribers_url":"https://api.github.com/repos/stephanie-wang/ray/subscribers","subscription_url":"https://api.github.com/repos/stephanie-wang/ray/subscription","commits_url":"https://api.github.com/repos/stephanie-wang/ray/commits{/sha}","git_commits_url":"https://api.github.com/repos/stephanie-wang/ray/git/commits{/sha}","comments_url":"https://api.github.com/repos/stephanie-wang/ray/comments{/number}","issue_comment_url":"https://api.github.com/repos/stephanie-wang/ray/issues/comments{/number}","contents_url":"https://api.github.com/repos/stephanie-wang/ray/contents/{+path}","compare_url":"https://api.github.com/repos/stephanie-wang/ray/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stephanie-wang/ray/merges","archive_url":"https://api.github.com/repos/stephanie-wang/ray/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stephanie-wang/ray/downloads","issues_url":"https://api.github.com/repos/stephanie-wang/ray/issues{/number}","pulls_url":"https://api.github.com/repos/stephanie-wang/ray/pulls{/number}","milestones_url":"https://api.github.com/repos/stephanie-wang/ray/milestones{/number}","notifications_url":"https://api.github.com/repos/stephanie-wang/ray/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stephanie-wang/ray/labels{/name}","releases_url":"https://api.github.com/repos/stephanie-wang/ray/releases{/id}","deployments_url":"https://api.github.com/repos/stephanie-wang/ray/deployments","created_at":"2016-12-24T22:37:23Z","updated_at":"2018-04-24T01:06:35Z","pushed_at":"2018-07-28T22:07:18Z","git_url":"git://github.com/stephanie-wang/ray.git","ssh_url":"git@github.com:stephanie-wang/ray.git","clone_url":"https://github.com/stephanie-wang/ray.git","svn_url":"https://github.com/stephanie-wang/ray","homepage":"","size":16613,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"ray-project:master","ref":"master","sha":"e10377567c4712eeb8cb856b997f22a92aa5dff9","user":{"login":"ray-project","id":22125274,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIyMTI1Mjc0","avatar_url":"https://avatars3.githubusercontent.com/u/22125274?v=4","gravatar_id":"","url":"https://api.github.com/users/ray-project","html_url":"https://github.com/ray-project","followers_url":"https://api.github.com/users/ray-project/followers","following_url":"https://api.github.com/users/ray-project/following{/other_user}","gists_url":"https://api.github.com/users/ray-project/gists{/gist_id}","starred_url":"https://api.github.com/users/ray-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ray-project/subscriptions","organizations_url":"https://api.github.com/users/ray-project/orgs","repos_url":"https://api.github.com/users/ray-project/repos","events_url":"https://api.github.com/users/ray-project/events{/privacy}","received_events_url":"https://api.github.com/users/ray-project/received_events","type":"Organization","site_admin":false},"repo":{"id":71932349,"node_id":"MDEwOlJlcG9zaXRvcnk3MTkzMjM0OQ==","name":"ray","full_name":"ray-project/ray","owner":{"login":"ray-project","id":22125274,"node_id":"MDEyOk9yZ2FuaXphdGlvbjIyMTI1Mjc0","avatar_url":"https://avatars3.githubusercontent.com/u/22125274?v=4","gravatar_id":"","url":"https://api.github.com/users/ray-project","html_url":"https://github.com/ray-project","followers_url":"https://api.github.com/users/ray-project/followers","following_url":"https://api.github.com/users/ray-project/following{/other_user}","gists_url":"https://api.github.com/users/ray-project/gists{/gist_id}","starred_url":"https://api.github.com/users/ray-project/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ray-project/subscriptions","organizations_url":"https://api.github.com/users/ray-project/orgs","repos_url":"https://api.github.com/users/ray-project/repos","events_url":"https://api.github.com/users/ray-project/events{/privacy}","received_events_url":"https://api.github.com/users/ray-project/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ray-project/ray","description":"A high-performance distributed execution engine","fork":false,"url":"https://api.github.com/repos/ray-project/ray","forks_url":"https://api.github.com/repos/ray-project/ray/forks","keys_url":"https://api.github.com/repos/ray-project/ray/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ray-project/ray/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ray-project/ray/teams","hooks_url":"https://api.github.com/repos/ray-project/ray/hooks","issue_events_url":"https://api.github.com/repos/ray-project/ray/issues/events{/number}","events_url":"https://api.github.com/repos/ray-project/ray/events","assignees_url":"https://api.github.com/repos/ray-project/ray/assignees{/user}","branches_url":"https://api.github.com/repos/ray-project/ray/branches{/branch}","tags_url":"https://api.github.com/repos/ray-project/ray/tags","blobs_url":"https://api.github.com/repos/ray-project/ray/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ray-project/ray/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ray-project/ray/git/refs{/sha}","trees_url":"https://api.github.com/repos/ray-project/ray/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ray-project/ray/statuses/{sha}","languages_url":"https://api.github.com/repos/ray-project/ray/languages","stargazers_url":"https://api.github.com/repos/ray-project/ray/stargazers","contributors_url":"https://api.github.com/repos/ray-project/ray/contributors","subscribers_url":"https://api.github.com/repos/ray-project/ray/subscribers","subscription_url":"https://api.github.com/repos/ray-project/ray/subscription","commits_url":"https://api.github.com/repos/ray-project/ray/commits{/sha}","git_commits_url":"https://api.github.com/repos/ray-project/ray/git/commits{/sha}","comments_url":"https://api.github.com/repos/ray-project/ray/comments{/number}","issue_comment_url":"https://api.github.com/repos/ray-project/ray/issues/comments{/number}","contents_url":"https://api.github.com/repos/ray-project/ray/contents/{+path}","compare_url":"https://api.github.com/repos/ray-project/ray/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ray-project/ray/merges","archive_url":"https://api.github.com/repos/ray-project/ray/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ray-project/ray/downloads","issues_url":"https://api.github.com/repos/ray-project/ray/issues{/number}","pulls_url":"https://api.github.com/repos/ray-project/ray/pulls{/number}","milestones_url":"https://api.github.com/repos/ray-project/ray/milestones{/number}","notifications_url":"https://api.github.com/repos/ray-project/ray/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ray-project/ray/labels{/name}","releases_url":"https://api.github.com/repos/ray-project/ray/releases{/id}","deployments_url":"https://api.github.com/repos/ray-project/ray/deployments","created_at":"2016-10-25T19:38:30Z","updated_at":"2018-07-30T03:41:18Z","pushed_at":"2018-07-30T04:02:12Z","git_url":"git://github.com/ray-project/ray.git","ssh_url":"git@github.com:ray-project/ray.git","clone_url":"https://github.com/ray-project/ray.git","svn_url":"https://github.com/ray-project/ray","homepage":"","size":13298,"stargazers_count":3796,"watchers_count":3796,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":506,"mirror_url":null,"archived":false,"open_issues_count":368,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":506,"open_issues":368,"watchers":3796,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ray-project/ray/pulls/2497"},"html":{"href":"https://github.com/ray-project/ray/pull/2497"},"issue":{"href":"https://api.github.com/repos/ray-project/ray/issues/2497"},"comments":{"href":"https://api.github.com/repos/ray-project/ray/issues/2497/comments"},"review_comments":{"href":"https://api.github.com/repos/ray-project/ray/pulls/2497/comments"},"review_comment":{"href":"https://api.github.com/repos/ray-project/ray/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ray-project/ray/pulls/2497/commits"},"statuses":{"href":"https://api.github.com/repos/ray-project/ray/statuses/5b27890d8165129d92018fe3a83a40b02db3fedf"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"stephanie-wang"}}