{"action":"created","comment":{"url":"https://api.github.com/repos/conveyal/r5/pulls/comments/166462971","pull_request_review_id":94528970,"id":166462971,"diff_hunk":"@@ -0,0 +1,141 @@\n+package com.conveyal.r5.analyst;\n+\n+import com.beust.jcommander.ParameterException;\n+import com.conveyal.r5.OneOriginResult;\n+import com.conveyal.r5.analyst.cluster.AnalysisTask;\n+import com.conveyal.r5.analyst.cluster.Origin;\n+import com.conveyal.r5.analyst.cluster.RegionalTask;\n+import com.conveyal.r5.analyst.cluster.TimeGrid;\n+import com.conveyal.r5.analyst.cluster.TravelTimeSurfaceTask;\n+import com.conveyal.r5.profile.FastRaptorWorker;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+import java.util.Arrays;\n+\n+/**\n+ * Given a bunch of travel times from an origin to a single destination grid cell, this collapses that long list into a\n+ * limited number of percentiles, then optionally accumulates that destination's opportunity count into the appropriate\n+ * cumulative opportunities accessibility indicators at that origin.\n+ */\n+public class TravelTimeReducer {\n+\n+    private static final Logger LOG = LoggerFactory.getLogger(TravelTimeReducer.class);\n+\n+    /** The task used to create travel times being reduced herein. */\n+    public final AnalysisTask task;\n+\n+    /** Travel time results for a whole grid of destinations. May be null if we're only recording accessibility. */\n+    private TimeGrid timeGrid = null;\n+\n+    private AccessibilityResult accessibilityResult = null;\n+\n+    private final boolean retainTravelTimes;\n+\n+    private final boolean calculateAccessibility;\n+\n+    private final int[] percentileIndexes;\n+\n+    private final int nPercentiles;\n+\n+    private final int timesPerDestination;\n+\n+    public TravelTimeReducer(AnalysisTask task) {\n+\n+        this.task = task;\n+        this.timesPerDestination = task.getMonteCarloDrawsPerMinute() * task.getTimeWindowLengthMinutes();\n+        this.nPercentiles = task.percentiles.length;\n+        this.percentileIndexes = new int[nPercentiles];\n+        for (int p = 0; p < nPercentiles; p++) {\n+            percentileIndexes[p] = findPercentileIndex(timesPerDestination, task.percentiles[p]);\n+        }\n+\n+        // Decide whether we want to retain travel times to all destinations for this origin.\n+        retainTravelTimes = task instanceof TravelTimeSurfaceTask || task.makeStaticSite;\n+        if (retainTravelTimes) {\n+            timeGrid = new TimeGrid(task.zoom, task.west, task.north, task.width, task.height, task.percentiles.length);\n+        }\n+\n+        // Decide whether we want to calculate cumulative opportunities accessibility indicators for this origin.\n+        calculateAccessibility = task instanceof RegionalTask && ((RegionalTask)task).gridData != null;\n+        if (calculateAccessibility) {\n+            accessibilityResult = new AccessibilityResult(\n+                new Grid[] {((RegionalTask)task).gridData},\n+                new int[]{task.maxTripDurationMinutes},\n+                task.percentiles\n+            );\n+        }\n+    }\n+\n+\n+    /**\n+     * Compute the index into a sorted list of N elements at which a particular percentile will be found.\n+     * Our method does not interpolate, it always reports a value actually appearing in the list of elements.\n+     * That is to say, the percentile will be found at an integer-valued index into the sorted array of elements.\n+     * Definition of non-interpolated percentile: the smallest value in the list such that no more than P percent of\n+     * the data is strictly less than the value and at least P percent of the data is less than or equal to that value.\n+     * The 100th percentile is defined as the largest value in the list.\n+     * See https://en.wikipedia.org/wiki/Percentile#Definitions\n+     */\n+    // We scale the interval between the beginning and end elements of the array (the min and max values).\n+    // In an array with N values this interval is N-1 elements. We should be scaling N-1, which makes the result\n+    // always defined even when using a high percentile and low number of elements.  Previously, this caused\n+    // an error below when requesting the 95th percentile when times.length = 1 (or any length less than 10).\n+    private static int findPercentileIndex(int nElements, double percentile) {\n+        // FIXME this should be floor not round. The definition uses ceiling for one-based indexes and we have zero-based indexes.\n+        return (int) Math.round(percentile / 100 * nElements);\n+    }\n+\n+    // TODO rename, this does not \"record\" the travel times, it consumes them or processes them\n+    public void recordTravelTimesForTarget (int target, int[] times) {\n+        // TODO factor out getPercentiles method for clarity\n+        // Sort the times at each target and read off percentiles at the pre-calculated indexes.\n+        int[] percentileTravelTimesMinutes = new int[nPercentiles];\n+        if (times.length == 1) {\n+            // Handle results with no variation\n+            // TODO instead of conditionals maybe overload this function to have one version that takes a single int time.\n+            Arrays.fill(percentileTravelTimesMinutes, times[0]);\n+        } else if (times.length == timesPerDestination) {\n+            Arrays.sort(times);\n+            for (int p = 0; p < nPercentiles; p++) {\n+                int timeSeconds = times[percentileIndexes[p]];\n+                if (timeSeconds == FastRaptorWorker.UNREACHED) {\n+                    percentileTravelTimesMinutes[p] = FastRaptorWorker.UNREACHED;\n+                } else {\n+                    // Int divide will floor; this is correct because value 0 has travel times of up to one minute, etc.\n+                    // This means that anything less than a cutoff of (say) 60 minutes (in seconds) will have value 59,\n+                    // which is what we want. But maybe converting to minutes before we actually export a binary format is tying\n+                    // the backend and frontend (which makes use of UInt8 typed arrays) too closely.\n+                    int timeMinutes = timeSeconds / 60;\n+                    percentileTravelTimesMinutes[p] = timeMinutes;\n+                }\n+            }\n+        } else {\n+            throw new ParameterException(\"Must supply expected number of times or only one time.\");","path":"src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java","position":114,"original_position":114,"commit_id":"20a4e1abf24426871645c374ff5b23e513d4d72a","original_commit_id":"20a4e1abf24426871645c374ff5b23e513d4d72a","user":{"login":"abyrd","id":112871,"avatar_url":"https://avatars0.githubusercontent.com/u/112871?v=4","gravatar_id":"","url":"https://api.github.com/users/abyrd","html_url":"https://github.com/abyrd","followers_url":"https://api.github.com/users/abyrd/followers","following_url":"https://api.github.com/users/abyrd/following{/other_user}","gists_url":"https://api.github.com/users/abyrd/gists{/gist_id}","starred_url":"https://api.github.com/users/abyrd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/abyrd/subscriptions","organizations_url":"https://api.github.com/users/abyrd/orgs","repos_url":"https://api.github.com/users/abyrd/repos","events_url":"https://api.github.com/users/abyrd/events{/privacy}","received_events_url":"https://api.github.com/users/abyrd/received_events","type":"User","site_admin":false},"body":"Fixed. @trevorgerhardt we should at least add a glossary to the Analysis documentation.","created_at":"2018-02-06T22:27:37Z","updated_at":"2018-02-06T22:27:38Z","html_url":"https://github.com/conveyal/r5/pull/399#discussion_r166462971","pull_request_url":"https://api.github.com/repos/conveyal/r5/pulls/399","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/conveyal/r5/pulls/comments/166462971"},"html":{"href":"https://github.com/conveyal/r5/pull/399#discussion_r166462971"},"pull_request":{"href":"https://api.github.com/repos/conveyal/r5/pulls/399"}},"in_reply_to_id":166134603},"pull_request":{"url":"https://api.github.com/repos/conveyal/r5/pulls/399","id":166927786,"html_url":"https://github.com/conveyal/r5/pull/399","diff_url":"https://github.com/conveyal/r5/pull/399.diff","patch_url":"https://github.com/conveyal/r5/pull/399.patch","issue_url":"https://api.github.com/repos/conveyal/r5/issues/399","number":399,"state":"open","locked":false,"title":"Analysis Simplifications: Broker and Worker","user":{"login":"abyrd","id":112871,"avatar_url":"https://avatars0.githubusercontent.com/u/112871?v=4","gravatar_id":"","url":"https://api.github.com/users/abyrd","html_url":"https://github.com/abyrd","followers_url":"https://api.github.com/users/abyrd/followers","following_url":"https://api.github.com/users/abyrd/following{/other_user}","gists_url":"https://api.github.com/users/abyrd/gists{/gist_id}","starred_url":"https://api.github.com/users/abyrd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/abyrd/subscriptions","organizations_url":"https://api.github.com/users/abyrd/orgs","repos_url":"https://api.github.com/users/abyrd/repos","events_url":"https://api.github.com/users/abyrd/events{/privacy}","received_events_url":"https://api.github.com/users/abyrd/received_events","type":"User","site_admin":false},"body":"This PR greatly simplifies several parts of Conveyal Analysis. The goal is to make the system more stable and maintainable, while paving the way for long-requested features. We want to eliminate deadlocks or other connection failures in the broker and make its memory consumption more predictable (near-constant if possible). The broker had mutated into a very tangled collection of code that matched up incoming HTTP connections from workers and the server, then had to deal with all sorts of problematic states.\r\n\r\nThese changes also make major progress toward allowing deployment of Analysis locally or in alternate hosting environments.\r\n\r\nThis PR makes breaking changes to the interface between workers and the server. It will make all past worker versions unusable. We should make sure to stabilize that new interface on staging before releasing this to production, unless we want to just release the server and worker in lockstep and not allow custom version selection.\r\n\r\nThis PR is tightly integrated with changes to analysis-backend in https://github.com/conveyal/analysis-backend/pull/113.\r\n\r\n1. The Broker:\r\n\r\n1a. The broker no longer stores separate objects for each task in a job. It stores one template task and produces the other tasks by cloning and modifying the template. This could eventually be taken even further by retaining the template tasks on the worker and cloning them there, with the broker only handing out the initial job description and task numbers to the workers. But the current system is an incremental change that targets the most significant improvements on the central server, where memory is often eaten up by the presence of multiple jobs. I don't really think the time spent doing the same thing on the workers would be very beneficial.\r\n\r\n1b. The broker is no longer a separate process, and not even a separate thread. It's just an object within the Analysis server that is written to and read from using synchronized methods called from HTTP API handler methods, or directly by the analysis-backend itself. Therefore *the broker is not even in the R5 project anymore*. After it was refactored I moved it over to analysis-backend. But of course that broker requires a bunch of other worker changes here in R5. This change removes all overhead and version-matching complexity of serializing jobs to JSON and deserializing them back into the broker, then serializing them again to send them to workers.\r\n\r\n2. Batch job tasks:\r\n\r\n2a. Workers still pull batch tasks from the broker, but they short-poll and wait instead of long polling. The long polling system was originally created to allow single point requests to be seen immediately by the workers, and is no longer relevant. This eliminates the need to hold connections open and detect connection closure, which eliminates all sorts of fragile custom code. Every HTTP request returns a result immediately.\r\n\r\n2b. Workers now return their regional results directly to the broker when polling for more work. This eliminates the need for an SQS queue to hold the results, decoupling us from AWS. The workers asynchronously poll for more work before they run out of tasks to work on, so even if there is contention for broker resources the worker should not block and will continue doing its job. The SQS queue appears to have been a way to work around the IOPS limits on our analysis server, whose IO would completely lock up after regional jobs created large files. We are no longer creating such big files and should shift to a server with provisioned IOPS anyway.\r\n\r\n3. Single-point tasks: We now push single-point tasks to an appropriate worker (rather than pull them via long-poll), and single-point requests don't go through the broker at all. The analysis-backend just proxies them to a worker that has the right graph loaded. This completely eliminates our most lockup-prone code and means that our API is expressed in a very classic way as a collection of simple imperative HTTP handler methods.\r\n\r\n4. Workers:\r\n\r\n4a. Producing static sites. Anson's work on producing static site files from within regional workers is fully enabled here. There is a new boolean variable `makeStaticSite` in the analysisRequest that will cause travel time surfaces and job metadata to be saved on S3 when enabled on a regional job.\r\n\r\n4b. Bootstrapping support is removed. The bootstrapping process estimates variation in the accessibility indicator due to the fact that we're using only a sample of possible schedules rather than testing them exhaustively. However it is only relevant when frequency-based lines are in use (which is often not the case for many of our users) and produces very voluminous results, whose storage had taken priority over the very useful feature of calculating indicators for many travel time cutoffs, percentiles, and destination sets at once. We have decided to use these statistical scenario comparison concepts, including the bootstrapping process, as a way to determine appropriate numbers of Monte Carlo draws and thresholds for significant change caused by scenarios, but not as something we will expose to customers. This calibration and sensitivity testing will be carried out by us as needed, and not on every analysis. Eliminating bootstrapping from this code base drastically simplifies it and increases maintainability.\r\n\r\n4c. Calculation methods have been repeatedly refactored to produce more conventional functions that transform an input type into an output type, rather than tail-calling layers onto the stack. The previous structure seems to have grown out of a streaming approach intended to handle the very large data sets produced by the bootstrapping process.\r\n","created_at":"2018-02-02T23:37:00Z","updated_at":"2018-02-06T22:27:38Z","closed_at":null,"merged_at":null,"merge_commit_sha":"944636d2a827c362e0a6aca246e8cd6bae38a2ae","assignee":null,"assignees":[],"requested_reviewers":[{"login":"trevorgerhardt","id":776780,"avatar_url":"https://avatars3.githubusercontent.com/u/776780?v=4","gravatar_id":"","url":"https://api.github.com/users/trevorgerhardt","html_url":"https://github.com/trevorgerhardt","followers_url":"https://api.github.com/users/trevorgerhardt/followers","following_url":"https://api.github.com/users/trevorgerhardt/following{/other_user}","gists_url":"https://api.github.com/users/trevorgerhardt/gists{/gist_id}","starred_url":"https://api.github.com/users/trevorgerhardt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/trevorgerhardt/subscriptions","organizations_url":"https://api.github.com/users/trevorgerhardt/orgs","repos_url":"https://api.github.com/users/trevorgerhardt/repos","events_url":"https://api.github.com/users/trevorgerhardt/events{/privacy}","received_events_url":"https://api.github.com/users/trevorgerhardt/received_events","type":"User","site_admin":false}],"requested_teams":[],"milestone":null,"commits_url":"https://api.github.com/repos/conveyal/r5/pulls/399/commits","review_comments_url":"https://api.github.com/repos/conveyal/r5/pulls/399/comments","review_comment_url":"https://api.github.com/repos/conveyal/r5/pulls/comments{/number}","comments_url":"https://api.github.com/repos/conveyal/r5/issues/399/comments","statuses_url":"https://api.github.com/repos/conveyal/r5/statuses/20a4e1abf24426871645c374ff5b23e513d4d72a","head":{"label":"conveyal:eliminate-broker","ref":"eliminate-broker","sha":"20a4e1abf24426871645c374ff5b23e513d4d72a","user":{"login":"conveyal","id":3592637,"avatar_url":"https://avatars0.githubusercontent.com/u/3592637?v=4","gravatar_id":"","url":"https://api.github.com/users/conveyal","html_url":"https://github.com/conveyal","followers_url":"https://api.github.com/users/conveyal/followers","following_url":"https://api.github.com/users/conveyal/following{/other_user}","gists_url":"https://api.github.com/users/conveyal/gists{/gist_id}","starred_url":"https://api.github.com/users/conveyal/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/conveyal/subscriptions","organizations_url":"https://api.github.com/users/conveyal/orgs","repos_url":"https://api.github.com/users/conveyal/repos","events_url":"https://api.github.com/users/conveyal/events{/privacy}","received_events_url":"https://api.github.com/users/conveyal/received_events","type":"Organization","site_admin":false},"repo":{"id":44746588,"name":"r5","full_name":"conveyal/r5","owner":{"login":"conveyal","id":3592637,"avatar_url":"https://avatars0.githubusercontent.com/u/3592637?v=4","gravatar_id":"","url":"https://api.github.com/users/conveyal","html_url":"https://github.com/conveyal","followers_url":"https://api.github.com/users/conveyal/followers","following_url":"https://api.github.com/users/conveyal/following{/other_user}","gists_url":"https://api.github.com/users/conveyal/gists{/gist_id}","starred_url":"https://api.github.com/users/conveyal/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/conveyal/subscriptions","organizations_url":"https://api.github.com/users/conveyal/orgs","repos_url":"https://api.github.com/users/conveyal/repos","events_url":"https://api.github.com/users/conveyal/events{/privacy}","received_events_url":"https://api.github.com/users/conveyal/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/conveyal/r5","description":"Rapid Realistic Routing on Real-world and Reimagined networks","fork":false,"url":"https://api.github.com/repos/conveyal/r5","forks_url":"https://api.github.com/repos/conveyal/r5/forks","keys_url":"https://api.github.com/repos/conveyal/r5/keys{/key_id}","collaborators_url":"https://api.github.com/repos/conveyal/r5/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/conveyal/r5/teams","hooks_url":"https://api.github.com/repos/conveyal/r5/hooks","issue_events_url":"https://api.github.com/repos/conveyal/r5/issues/events{/number}","events_url":"https://api.github.com/repos/conveyal/r5/events","assignees_url":"https://api.github.com/repos/conveyal/r5/assignees{/user}","branches_url":"https://api.github.com/repos/conveyal/r5/branches{/branch}","tags_url":"https://api.github.com/repos/conveyal/r5/tags","blobs_url":"https://api.github.com/repos/conveyal/r5/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/conveyal/r5/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/conveyal/r5/git/refs{/sha}","trees_url":"https://api.github.com/repos/conveyal/r5/git/trees{/sha}","statuses_url":"https://api.github.com/repos/conveyal/r5/statuses/{sha}","languages_url":"https://api.github.com/repos/conveyal/r5/languages","stargazers_url":"https://api.github.com/repos/conveyal/r5/stargazers","contributors_url":"https://api.github.com/repos/conveyal/r5/contributors","subscribers_url":"https://api.github.com/repos/conveyal/r5/subscribers","subscription_url":"https://api.github.com/repos/conveyal/r5/subscription","commits_url":"https://api.github.com/repos/conveyal/r5/commits{/sha}","git_commits_url":"https://api.github.com/repos/conveyal/r5/git/commits{/sha}","comments_url":"https://api.github.com/repos/conveyal/r5/comments{/number}","issue_comment_url":"https://api.github.com/repos/conveyal/r5/issues/comments{/number}","contents_url":"https://api.github.com/repos/conveyal/r5/contents/{+path}","compare_url":"https://api.github.com/repos/conveyal/r5/compare/{base}...{head}","merges_url":"https://api.github.com/repos/conveyal/r5/merges","archive_url":"https://api.github.com/repos/conveyal/r5/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/conveyal/r5/downloads","issues_url":"https://api.github.com/repos/conveyal/r5/issues{/number}","pulls_url":"https://api.github.com/repos/conveyal/r5/pulls{/number}","milestones_url":"https://api.github.com/repos/conveyal/r5/milestones{/number}","notifications_url":"https://api.github.com/repos/conveyal/r5/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/conveyal/r5/labels{/name}","releases_url":"https://api.github.com/repos/conveyal/r5/releases{/id}","deployments_url":"https://api.github.com/repos/conveyal/r5/deployments","created_at":"2015-10-22T13:23:49Z","updated_at":"2018-01-23T07:52:33Z","pushed_at":"2018-02-02T23:37:01Z","git_url":"git://github.com/conveyal/r5.git","ssh_url":"git@github.com:conveyal/r5.git","clone_url":"https://github.com/conveyal/r5.git","svn_url":"https://github.com/conveyal/r5","homepage":null,"size":34417,"stargazers_count":40,"watchers_count":40,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":7,"mirror_url":null,"archived":false,"open_issues_count":151,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":7,"open_issues":151,"watchers":40,"default_branch":"dev"}},"base":{"label":"conveyal:dev","ref":"dev","sha":"627e5c89bfef24b0ab32e7c9aeb91858d30d0482","user":{"login":"conveyal","id":3592637,"avatar_url":"https://avatars0.githubusercontent.com/u/3592637?v=4","gravatar_id":"","url":"https://api.github.com/users/conveyal","html_url":"https://github.com/conveyal","followers_url":"https://api.github.com/users/conveyal/followers","following_url":"https://api.github.com/users/conveyal/following{/other_user}","gists_url":"https://api.github.com/users/conveyal/gists{/gist_id}","starred_url":"https://api.github.com/users/conveyal/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/conveyal/subscriptions","organizations_url":"https://api.github.com/users/conveyal/orgs","repos_url":"https://api.github.com/users/conveyal/repos","events_url":"https://api.github.com/users/conveyal/events{/privacy}","received_events_url":"https://api.github.com/users/conveyal/received_events","type":"Organization","site_admin":false},"repo":{"id":44746588,"name":"r5","full_name":"conveyal/r5","owner":{"login":"conveyal","id":3592637,"avatar_url":"https://avatars0.githubusercontent.com/u/3592637?v=4","gravatar_id":"","url":"https://api.github.com/users/conveyal","html_url":"https://github.com/conveyal","followers_url":"https://api.github.com/users/conveyal/followers","following_url":"https://api.github.com/users/conveyal/following{/other_user}","gists_url":"https://api.github.com/users/conveyal/gists{/gist_id}","starred_url":"https://api.github.com/users/conveyal/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/conveyal/subscriptions","organizations_url":"https://api.github.com/users/conveyal/orgs","repos_url":"https://api.github.com/users/conveyal/repos","events_url":"https://api.github.com/users/conveyal/events{/privacy}","received_events_url":"https://api.github.com/users/conveyal/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/conveyal/r5","description":"Rapid Realistic Routing on Real-world and Reimagined networks","fork":false,"url":"https://api.github.com/repos/conveyal/r5","forks_url":"https://api.github.com/repos/conveyal/r5/forks","keys_url":"https://api.github.com/repos/conveyal/r5/keys{/key_id}","collaborators_url":"https://api.github.com/repos/conveyal/r5/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/conveyal/r5/teams","hooks_url":"https://api.github.com/repos/conveyal/r5/hooks","issue_events_url":"https://api.github.com/repos/conveyal/r5/issues/events{/number}","events_url":"https://api.github.com/repos/conveyal/r5/events","assignees_url":"https://api.github.com/repos/conveyal/r5/assignees{/user}","branches_url":"https://api.github.com/repos/conveyal/r5/branches{/branch}","tags_url":"https://api.github.com/repos/conveyal/r5/tags","blobs_url":"https://api.github.com/repos/conveyal/r5/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/conveyal/r5/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/conveyal/r5/git/refs{/sha}","trees_url":"https://api.github.com/repos/conveyal/r5/git/trees{/sha}","statuses_url":"https://api.github.com/repos/conveyal/r5/statuses/{sha}","languages_url":"https://api.github.com/repos/conveyal/r5/languages","stargazers_url":"https://api.github.com/repos/conveyal/r5/stargazers","contributors_url":"https://api.github.com/repos/conveyal/r5/contributors","subscribers_url":"https://api.github.com/repos/conveyal/r5/subscribers","subscription_url":"https://api.github.com/repos/conveyal/r5/subscription","commits_url":"https://api.github.com/repos/conveyal/r5/commits{/sha}","git_commits_url":"https://api.github.com/repos/conveyal/r5/git/commits{/sha}","comments_url":"https://api.github.com/repos/conveyal/r5/comments{/number}","issue_comment_url":"https://api.github.com/repos/conveyal/r5/issues/comments{/number}","contents_url":"https://api.github.com/repos/conveyal/r5/contents/{+path}","compare_url":"https://api.github.com/repos/conveyal/r5/compare/{base}...{head}","merges_url":"https://api.github.com/repos/conveyal/r5/merges","archive_url":"https://api.github.com/repos/conveyal/r5/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/conveyal/r5/downloads","issues_url":"https://api.github.com/repos/conveyal/r5/issues{/number}","pulls_url":"https://api.github.com/repos/conveyal/r5/pulls{/number}","milestones_url":"https://api.github.com/repos/conveyal/r5/milestones{/number}","notifications_url":"https://api.github.com/repos/conveyal/r5/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/conveyal/r5/labels{/name}","releases_url":"https://api.github.com/repos/conveyal/r5/releases{/id}","deployments_url":"https://api.github.com/repos/conveyal/r5/deployments","created_at":"2015-10-22T13:23:49Z","updated_at":"2018-01-23T07:52:33Z","pushed_at":"2018-02-02T23:37:01Z","git_url":"git://github.com/conveyal/r5.git","ssh_url":"git@github.com:conveyal/r5.git","clone_url":"https://github.com/conveyal/r5.git","svn_url":"https://github.com/conveyal/r5","homepage":null,"size":34417,"stargazers_count":40,"watchers_count":40,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":7,"mirror_url":null,"archived":false,"open_issues_count":151,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":7,"open_issues":151,"watchers":40,"default_branch":"dev"}},"_links":{"self":{"href":"https://api.github.com/repos/conveyal/r5/pulls/399"},"html":{"href":"https://github.com/conveyal/r5/pull/399"},"issue":{"href":"https://api.github.com/repos/conveyal/r5/issues/399"},"comments":{"href":"https://api.github.com/repos/conveyal/r5/issues/399/comments"},"review_comments":{"href":"https://api.github.com/repos/conveyal/r5/pulls/399/comments"},"review_comment":{"href":"https://api.github.com/repos/conveyal/r5/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/conveyal/r5/pulls/399/commits"},"statuses":{"href":"https://api.github.com/repos/conveyal/r5/statuses/20a4e1abf24426871645c374ff5b23e513d4d72a"}},"author_association":"OWNER"}}
{"actor":{"display_login":"abyrd"}}