{"action":"created","comment":{"url":"https://api.github.com/repos/VoltDB/voltdb/pulls/comments/62220211","id":62220211,"diff_hunk":"@@ -0,0 +1,369 @@\n+/* This file is part of VoltDB.\n+ * Copyright (C) 2008-2016 VoltDB Inc.\n+ *\n+ * This program is free software: you can redistribute it and/or modify\n+ * it under the terms of the GNU Affero General Public License as\n+ * published by the Free Software Foundation, either version 3 of the\n+ * License, or (at your option) any later version.\n+ *\n+ * This program is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+ * GNU Affero General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Affero General Public License\n+ * along with VoltDB.  If not, see <http://www.gnu.org/licenses/>.\n+ */\n+\n+package org.voltdb.importclient.kinesis;\n+\n+import java.io.UnsupportedEncodingException;\n+import java.lang.reflect.Array;\n+import java.math.BigInteger;\n+import java.net.URI;\n+import java.util.UUID;\n+import java.util.concurrent.atomic.AtomicLong;\n+\n+import org.apache.commons.lang.math.NumberUtils;\n+import org.voltcore.logging.Level;\n+import org.voltdb.client.ClientResponse;\n+import org.voltdb.client.ProcedureCallback;\n+import org.voltdb.importer.AbstractImporter;\n+import org.voltdb.importer.Invocation;\n+import org.voltdb.importer.formatter.FormatException;\n+import org.voltdb.importer.formatter.Formatter;\n+\n+import com.amazonaws.AmazonClientException;\n+import com.amazonaws.auth.AWSCredentialsProvider;\n+import com.amazonaws.auth.BasicAWSCredentials;\n+import com.amazonaws.internal.StaticCredentialsProvider;\n+import com.amazonaws.services.kinesis.clientlibrary.exceptions.ThrottlingException;\n+import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer;\n+import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor;\n+import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessorFactory;\n+import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream;\n+import com.amazonaws.services.kinesis.clientlibrary.lib.worker.KinesisClientLibConfiguration;\n+import com.amazonaws.services.kinesis.clientlibrary.lib.worker.Worker;\n+import com.amazonaws.services.kinesis.clientlibrary.types.InitializationInput;\n+import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput;\n+import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownInput;\n+import com.amazonaws.services.kinesis.clientlibrary.types.ShutdownReason;\n+import com.amazonaws.services.kinesis.model.Record;\n+\n+/**\n+ * Importer implementation for Kinesis Stream importer. one instance of this per stream, per shard, per app\n+ */\n+public class KinesisStreamImporter extends AbstractImporter {\n+\n+    private KinesisStreamImporterConfig m_config;\n+    private AtomicLong m_submitCount = new AtomicLong(0);\n+    private AtomicLong m_cbcnt = new AtomicLong(0);\n+\n+    private Worker m_worker;\n+\n+    public KinesisStreamImporter(KinesisStreamImporterConfig config) {\n+        m_config = config;\n+    }\n+\n+    @Override\n+    public URI getResourceID() {\n+        return m_config.getResourceID();\n+    }\n+\n+    @Override\n+    public void accept() {\n+\n+        info(null, \"Starting data stream fetcher for \" + m_config.getResourceID().toString());\n+        try {\n+            KinesisClientLibConfiguration kclConfig = new KinesisClientLibConfiguration(m_config.getAppName(),\n+                    m_config.getStreamName(), credentials(), UUID.randomUUID().toString());\n+\n+            kclConfig.withRegionName(m_config.getRegion()).withMaxRecords((int) m_config.getMaxReadBatchSize())\n+                    .withInitialPositionInStream(InitialPositionInStream.TRIM_HORIZON)\n+                    .withIdleTimeBetweenReadsInMillis(m_config.getIdleTimeBetweenReads())\n+                    .withTaskBackoffTimeMillis(m_config.getTaskBackoffTimeMillis()).withKinesisClientConfig(\n+                            KinesisStreamImporterConfig.getClientConfigWithUserAgent(m_config.getAppName()));\n+\n+            m_worker = new Worker.Builder().recordProcessorFactory(new RecordProcessorFactory()).config(kclConfig)\n+                    .build();\n+\n+            m_worker.run();\n+\n+        } catch (Exception e) {\n+\n+            rateLimitedLog(Level.ERROR, e, \"Error in Kinesis stream importer %s\", m_config.getResourceID());\n+            if (null != m_worker)\n+                m_worker.shutdown();\n+\n+        }\n+\n+        info(null, \"Data stream fetcher stopped for %s. Callback Rcvd: %d. Submitted: %d\",\n+                m_config.getResourceID().toString(), m_cbcnt.get(), m_submitCount.get());\n+\n+    }\n+\n+    @Override\n+    public void stop() {\n+        if (null != m_worker) {\n+            m_worker.shutdown();\n+        }\n+    }\n+\n+    @Override\n+    public String getName() {\n+        return KinesisStreamImporterConfig.APP_NAME;\n+    }\n+\n+    /**\n+     * Create AWSCredentialsProvider with access key id and secret key for the\n+     * user. The user should have read/write permission to Kinesis Stream and DynamoDB\n+     * @return AWSCredentialsProvider Provides credentials used to sign AWS requests\n+     * @throws AmazonClientException\n+     */\n+    public AWSCredentialsProvider credentials() throws AmazonClientException {\n+        return new StaticCredentialsProvider(new BasicAWSCredentials(m_config.getAccessKey(), m_config.getSecretKey()));\n+    }\n+\n+    private class RecordProcessorFactory implements IRecordProcessorFactory {\n+\n+        @Override\n+        public IRecordProcessor createProcessor() {\n+            return new StreamConsumer();\n+        }\n+    }\n+\n+    private class StreamConsumer implements IRecordProcessor {\n+\n+        private String m_shardId;\n+        private Formatter<String> m_formatter;\n+        Gap m_gapTracker = new Gap(Integer.getInteger(\"KINESIS_IMPORT_GAP_LEAD\", 32768));\n+        private BigInteger m_lastFetchCommittedSequenceNumber = BigInteger.ZERO;\n+\n+        public StreamConsumer() {\n+        }\n+\n+        @SuppressWarnings(\"unchecked\")\n+        @Override\n+        public void initialize(InitializationInput initInput) {\n+\n+            m_shardId = initInput.getShardId();\n+            m_formatter = ((Formatter<String>) m_config.getFormatterFactory().create());\n+\n+            String seq = initInput.getExtendedSequenceNumber().getSequenceNumber();\n+            if (NumberUtils.isDigits(seq)) {\n+                m_lastFetchCommittedSequenceNumber = new BigInteger(seq);\n+            }\n+\n+            info(null, \"Initializing Kinesis stream processing for shard %s, last committed on: %s\", m_shardId, seq);\n+        }\n+\n+        @Override\n+        public void processRecords(ProcessRecordsInput records) {\n+\n+            if (records.getRecords().isEmpty()) {\n+                return;\n+            }\n+\n+            BigInteger seq = BigInteger.ZERO;\n+            m_gapTracker.allocate(records.getRecords().size());\n+            int offset = 0;\n+            for (Record record : records.getRecords()) {\n+                m_submitCount.incrementAndGet();\n+                BigInteger seqNum = new BigInteger(record.getSequenceNumber());\n+                if (seqNum.compareTo(m_lastFetchCommittedSequenceNumber) < 0) {\n+                    continue;\n+                }\n+\n+                if (isDebugEnabled()) {\n+                    debug(null, \"last committed seq: %s, current seq:%s shard %s\",\n+                            m_lastFetchCommittedSequenceNumber.toString(), record.getSequenceNumber(), m_shardId);\n+                    if (seqNum.compareTo(seq) < 0) {\n+                        debug(null, \"Record %d is out of sequence on shard %s\", seqNum, m_shardId);\n+                    } else {\n+                        seq = seqNum;\n+                    }\n+                }\n+\n+                String data = null;\n+                try {\n+                    data = new String(record.getData().array(), \"UTF-8\");\n+                    Invocation invocation = new Invocation(m_config.getProcedure(), m_formatter.transform(data));\n+                    StreamProcedureCallback cb = new StreamProcedureCallback(m_gapTracker, offset, seqNum, m_cbcnt);\n+                    if (!callProcedure(invocation, cb)) {\n+                        rateLimitedLog(Level.ERROR, null, \"Call procedure error on shard %s\", m_shardId);\n+                        m_gapTracker.commit(offset, seqNum);\n+                    }\n+                } catch (UnsupportedEncodingException | FormatException e) {\n+                    rateLimitedLog(Level.ERROR, e, \"Data error on shard %s, data: %s\", m_shardId, data);\n+                    m_gapTracker.commit(offset, seqNum);\n+                } catch (Exception e) {\n+                    rateLimitedLog(Level.ERROR, e, \"Call procedure error with data %s on shard %s\", data, m_shardId);\n+                    break;\n+                }\n+                if (!shouldRun()) {\n+                    break;\n+                }\n+                offset++;\n+            }\n+\n+            commitCheckPoint(records.getCheckpointer());\n+        }\n+\n+        @Override\n+        public void shutdown(ShutdownInput shutDownInput) {\n+\n+            if (shutDownInput.getShutdownReason().equals(ShutdownReason.TERMINATE)) {\n+                //The shard may be split or merged. checkpoint one last time\n+                commitCheckPoint(shutDownInput.getCheckpointer());\n+            }\n+        }\n+\n+        /**\n+        * set a checkpoint to dynamoDB so we know the records prior to the point are processed by this app\n+        * @param checkpointer  The checkpoint processor\n+        */\n+        private void commitCheckPoint(IRecordProcessorCheckpointer checkpointer) {\n+\n+            int retries = 1;\n+            while (retries < 4 && shouldRun()) {\n+\n+                final BigInteger safe = m_gapTracker.getSafeCommitPoint();\n+                if (safe == null) {\n+                    break;\n+                }\n+                if (isDebugEnabled()) {\n+                    debug(null, \"New checkpoint %s, last checkpoint %s on shard %s\", safe.toString(),\n+                            m_lastFetchCommittedSequenceNumber.toString(), m_shardId);\n+                }\n+                if (safe.compareTo(m_lastFetchCommittedSequenceNumber) > 0) {\n+\n+                    if (isDebugEnabled()) {\n+                        debug(null, \"Trying to checkpoint %s on shard %s\", safe.toString(), m_shardId);\n+                    }\n+\n+                    try {\n+                        checkpointer.checkpoint(safe.toString());\n+                        m_lastFetchCommittedSequenceNumber = safe;\n+                        break;\n+                    } catch (ThrottlingException e) {\n+                        rateLimitedLog(Level.INFO, null, \"Checkpoint attempt  %d on shard %s\", retries, m_shardId);\n+                    } catch (Exception e) {\n+                        //committed on other nodes\n+                        rateLimitedLog(Level.WARN, e, \"Skipping checkpoint %s on shard %s. Reason: %s\", safe.toString(),\n+                                m_shardId, e.getMessage());\n+                        break;\n+                    }\n+                }\n+                backoffSleep(retries++);\n+            }\n+        }\n+    }\n+\n+    private int backoffSleep(int failedCount) {\n+        try {\n+            Thread.sleep(1000 * failedCount++);\n+            if (failedCount > 10)\n+                failedCount = 1;\n+        } catch (InterruptedException e) {\n+            rateLimitedLog(Level.ERROR, e, \"Interrupted sleep when checkpointing.\");\n+        }\n+        return failedCount;\n+    }\n+\n+    private final static class StreamProcedureCallback implements ProcedureCallback {\n+\n+        private final Gap m_tracker;\n+        private final int m_offset;\n+        private final BigInteger m_seq;\n+        private final AtomicLong m_cbcnt;\n+\n+        public StreamProcedureCallback(final Gap tracker, final int offset, BigInteger seq, AtomicLong cbcnt) {\n+\n+            m_tracker = tracker;\n+            m_offset = offset;\n+            m_seq = seq;\n+            m_cbcnt = cbcnt;\n+            m_tracker.submit(m_offset, m_seq);\n+        }\n+\n+        @Override\n+        public void clientCallback(ClientResponse response) throws Exception {\n+            m_tracker.commit(m_offset, m_seq);\n+            m_cbcnt.incrementAndGet();\n+        }\n+    }\n+\n+    /**\n+     * The class take an array of checkpoint objects and use their indices within the array as offsets to keep track of the safe\n+     * commit point. use the safe commit offset to look up the commit point for target system.\n+     */\n+    final class Gap {\n+        long c = 0;\n+        long s = -1L;\n+        long[] lag;\n+        final int lagLen;\n+        BigInteger[] chceckpoints;\n+\n+        Gap(int leeway) {\n+            if (leeway <= 0) {\n+                throw new IllegalArgumentException(\"leeways is zero or negative\");\n+            }\n+            lagLen = leeway;\n+        }\n+\n+        synchronized void allocate(int capacity) {\n+\n+            chceckpoints = (BigInteger[]) Array.newInstance(BigInteger.class, capacity);\n+            c = 0;\n+            s = -1L;\n+            lag = new long[lagLen];\n+        }\n+\n+        synchronized void submit(long offset, BigInteger v) {\n+\n+            if (!validateOffset((int) offset) || v == null || chceckpoints[(int) offset] != null) {\n+                return;","path":"src/frontend/org/voltdb/importclient/kinesis/KinesisStreamImporter.java","position":325,"original_position":325,"commit_id":"8d521c48ea9ddce82cc97b906978e2e9203cd00c","original_commit_id":"8d521c48ea9ddce82cc97b906978e2e9203cd00c","user":{"login":"vtkstef","id":662716,"avatar_url":"https://avatars.githubusercontent.com/u/662716?v=3","gravatar_id":"","url":"https://api.github.com/users/vtkstef","html_url":"https://github.com/vtkstef","followers_url":"https://api.github.com/users/vtkstef/followers","following_url":"https://api.github.com/users/vtkstef/following{/other_user}","gists_url":"https://api.github.com/users/vtkstef/gists{/gist_id}","starred_url":"https://api.github.com/users/vtkstef/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/vtkstef/subscriptions","organizations_url":"https://api.github.com/users/vtkstef/orgs","repos_url":"https://api.github.com/users/vtkstef/repos","events_url":"https://api.github.com/users/vtkstef/events{/privacy}","received_events_url":"https://api.github.com/users/vtkstef/received_events","type":"User","site_admin":false},"body":"modify it as it was implemented in ENG-10264","created_at":"2016-05-05T17:18:30Z","updated_at":"2016-05-05T17:18:30Z","html_url":"https://github.com/VoltDB/voltdb/pull/3541#discussion_r62220211","pull_request_url":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541","_links":{"self":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/comments/62220211"},"html":{"href":"https://github.com/VoltDB/voltdb/pull/3541#discussion_r62220211"},"pull_request":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541"}}},"pull_request":{"url":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541","id":68556149,"html_url":"https://github.com/VoltDB/voltdb/pull/3541","diff_url":"https://github.com/VoltDB/voltdb/pull/3541.diff","patch_url":"https://github.com/VoltDB/voltdb/pull/3541.patch","issue_url":"https://api.github.com/repos/VoltDB/voltdb/issues/3541","number":3541,"state":"open","locked":false,"title":"ENG-10086 Kinesis Stream Importer","user":{"login":"zeemanhe","id":18117799,"avatar_url":"https://avatars.githubusercontent.com/u/18117799?v=3","gravatar_id":"","url":"https://api.github.com/users/zeemanhe","html_url":"https://github.com/zeemanhe","followers_url":"https://api.github.com/users/zeemanhe/followers","following_url":"https://api.github.com/users/zeemanhe/following{/other_user}","gists_url":"https://api.github.com/users/zeemanhe/gists{/gist_id}","starred_url":"https://api.github.com/users/zeemanhe/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zeemanhe/subscriptions","organizations_url":"https://api.github.com/users/zeemanhe/orgs","repos_url":"https://api.github.com/users/zeemanhe/repos","events_url":"https://api.github.com/users/zeemanhe/events{/privacy}","received_events_url":"https://api.github.com/users/zeemanhe/received_events","type":"User","site_admin":false},"body":"Kinesis stream importer with AWS SDK and client lib.","created_at":"2016-05-02T12:55:12Z","updated_at":"2016-05-05T17:18:30Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0ab6db2b63fb67e590da1cf9313014c23dcba196","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541/commits","review_comments_url":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541/comments","review_comment_url":"https://api.github.com/repos/VoltDB/voltdb/pulls/comments{/number}","comments_url":"https://api.github.com/repos/VoltDB/voltdb/issues/3541/comments","statuses_url":"https://api.github.com/repos/VoltDB/voltdb/statuses/8d521c48ea9ddce82cc97b906978e2e9203cd00c","head":{"label":"VoltDB:ENG-10086","ref":"ENG-10086","sha":"8d521c48ea9ddce82cc97b906978e2e9203cd00c","user":{"login":"VoltDB","id":703910,"avatar_url":"https://avatars.githubusercontent.com/u/703910?v=3","gravatar_id":"","url":"https://api.github.com/users/VoltDB","html_url":"https://github.com/VoltDB","followers_url":"https://api.github.com/users/VoltDB/followers","following_url":"https://api.github.com/users/VoltDB/following{/other_user}","gists_url":"https://api.github.com/users/VoltDB/gists{/gist_id}","starred_url":"https://api.github.com/users/VoltDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/VoltDB/subscriptions","organizations_url":"https://api.github.com/users/VoltDB/orgs","repos_url":"https://api.github.com/users/VoltDB/repos","events_url":"https://api.github.com/users/VoltDB/events{/privacy}","received_events_url":"https://api.github.com/users/VoltDB/received_events","type":"Organization","site_admin":false},"repo":{"id":2445970,"name":"voltdb","full_name":"VoltDB/voltdb","owner":{"login":"VoltDB","id":703910,"avatar_url":"https://avatars.githubusercontent.com/u/703910?v=3","gravatar_id":"","url":"https://api.github.com/users/VoltDB","html_url":"https://github.com/VoltDB","followers_url":"https://api.github.com/users/VoltDB/followers","following_url":"https://api.github.com/users/VoltDB/following{/other_user}","gists_url":"https://api.github.com/users/VoltDB/gists{/gist_id}","starred_url":"https://api.github.com/users/VoltDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/VoltDB/subscriptions","organizations_url":"https://api.github.com/users/VoltDB/orgs","repos_url":"https://api.github.com/users/VoltDB/repos","events_url":"https://api.github.com/users/VoltDB/events{/privacy}","received_events_url":"https://api.github.com/users/VoltDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/VoltDB/voltdb","description":"VoltDB","fork":false,"url":"https://api.github.com/repos/VoltDB/voltdb","forks_url":"https://api.github.com/repos/VoltDB/voltdb/forks","keys_url":"https://api.github.com/repos/VoltDB/voltdb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/VoltDB/voltdb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/VoltDB/voltdb/teams","hooks_url":"https://api.github.com/repos/VoltDB/voltdb/hooks","issue_events_url":"https://api.github.com/repos/VoltDB/voltdb/issues/events{/number}","events_url":"https://api.github.com/repos/VoltDB/voltdb/events","assignees_url":"https://api.github.com/repos/VoltDB/voltdb/assignees{/user}","branches_url":"https://api.github.com/repos/VoltDB/voltdb/branches{/branch}","tags_url":"https://api.github.com/repos/VoltDB/voltdb/tags","blobs_url":"https://api.github.com/repos/VoltDB/voltdb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/VoltDB/voltdb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/VoltDB/voltdb/git/refs{/sha}","trees_url":"https://api.github.com/repos/VoltDB/voltdb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/VoltDB/voltdb/statuses/{sha}","languages_url":"https://api.github.com/repos/VoltDB/voltdb/languages","stargazers_url":"https://api.github.com/repos/VoltDB/voltdb/stargazers","contributors_url":"https://api.github.com/repos/VoltDB/voltdb/contributors","subscribers_url":"https://api.github.com/repos/VoltDB/voltdb/subscribers","subscription_url":"https://api.github.com/repos/VoltDB/voltdb/subscription","commits_url":"https://api.github.com/repos/VoltDB/voltdb/commits{/sha}","git_commits_url":"https://api.github.com/repos/VoltDB/voltdb/git/commits{/sha}","comments_url":"https://api.github.com/repos/VoltDB/voltdb/comments{/number}","issue_comment_url":"https://api.github.com/repos/VoltDB/voltdb/issues/comments{/number}","contents_url":"https://api.github.com/repos/VoltDB/voltdb/contents/{+path}","compare_url":"https://api.github.com/repos/VoltDB/voltdb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/VoltDB/voltdb/merges","archive_url":"https://api.github.com/repos/VoltDB/voltdb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/VoltDB/voltdb/downloads","issues_url":"https://api.github.com/repos/VoltDB/voltdb/issues{/number}","pulls_url":"https://api.github.com/repos/VoltDB/voltdb/pulls{/number}","milestones_url":"https://api.github.com/repos/VoltDB/voltdb/milestones{/number}","notifications_url":"https://api.github.com/repos/VoltDB/voltdb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/VoltDB/voltdb/labels{/name}","releases_url":"https://api.github.com/repos/VoltDB/voltdb/releases{/id}","deployments_url":"https://api.github.com/repos/VoltDB/voltdb/deployments","created_at":"2011-09-23T18:46:12Z","updated_at":"2016-05-05T04:58:24Z","pushed_at":"2016-05-05T17:14:08Z","git_url":"git://github.com/VoltDB/voltdb.git","ssh_url":"git@github.com:VoltDB/voltdb.git","clone_url":"https://github.com/VoltDB/voltdb.git","svn_url":"https://github.com/VoltDB/voltdb","homepage":"http://community.voltdb.com/","size":626947,"stargazers_count":766,"watchers_count":766,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":235,"mirror_url":null,"open_issues_count":33,"forks":235,"open_issues":33,"watchers":766,"default_branch":"master"}},"base":{"label":"VoltDB:master","ref":"master","sha":"103a81f812a74e774ebcf387ed00b113339ebdea","user":{"login":"VoltDB","id":703910,"avatar_url":"https://avatars.githubusercontent.com/u/703910?v=3","gravatar_id":"","url":"https://api.github.com/users/VoltDB","html_url":"https://github.com/VoltDB","followers_url":"https://api.github.com/users/VoltDB/followers","following_url":"https://api.github.com/users/VoltDB/following{/other_user}","gists_url":"https://api.github.com/users/VoltDB/gists{/gist_id}","starred_url":"https://api.github.com/users/VoltDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/VoltDB/subscriptions","organizations_url":"https://api.github.com/users/VoltDB/orgs","repos_url":"https://api.github.com/users/VoltDB/repos","events_url":"https://api.github.com/users/VoltDB/events{/privacy}","received_events_url":"https://api.github.com/users/VoltDB/received_events","type":"Organization","site_admin":false},"repo":{"id":2445970,"name":"voltdb","full_name":"VoltDB/voltdb","owner":{"login":"VoltDB","id":703910,"avatar_url":"https://avatars.githubusercontent.com/u/703910?v=3","gravatar_id":"","url":"https://api.github.com/users/VoltDB","html_url":"https://github.com/VoltDB","followers_url":"https://api.github.com/users/VoltDB/followers","following_url":"https://api.github.com/users/VoltDB/following{/other_user}","gists_url":"https://api.github.com/users/VoltDB/gists{/gist_id}","starred_url":"https://api.github.com/users/VoltDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/VoltDB/subscriptions","organizations_url":"https://api.github.com/users/VoltDB/orgs","repos_url":"https://api.github.com/users/VoltDB/repos","events_url":"https://api.github.com/users/VoltDB/events{/privacy}","received_events_url":"https://api.github.com/users/VoltDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/VoltDB/voltdb","description":"VoltDB","fork":false,"url":"https://api.github.com/repos/VoltDB/voltdb","forks_url":"https://api.github.com/repos/VoltDB/voltdb/forks","keys_url":"https://api.github.com/repos/VoltDB/voltdb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/VoltDB/voltdb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/VoltDB/voltdb/teams","hooks_url":"https://api.github.com/repos/VoltDB/voltdb/hooks","issue_events_url":"https://api.github.com/repos/VoltDB/voltdb/issues/events{/number}","events_url":"https://api.github.com/repos/VoltDB/voltdb/events","assignees_url":"https://api.github.com/repos/VoltDB/voltdb/assignees{/user}","branches_url":"https://api.github.com/repos/VoltDB/voltdb/branches{/branch}","tags_url":"https://api.github.com/repos/VoltDB/voltdb/tags","blobs_url":"https://api.github.com/repos/VoltDB/voltdb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/VoltDB/voltdb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/VoltDB/voltdb/git/refs{/sha}","trees_url":"https://api.github.com/repos/VoltDB/voltdb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/VoltDB/voltdb/statuses/{sha}","languages_url":"https://api.github.com/repos/VoltDB/voltdb/languages","stargazers_url":"https://api.github.com/repos/VoltDB/voltdb/stargazers","contributors_url":"https://api.github.com/repos/VoltDB/voltdb/contributors","subscribers_url":"https://api.github.com/repos/VoltDB/voltdb/subscribers","subscription_url":"https://api.github.com/repos/VoltDB/voltdb/subscription","commits_url":"https://api.github.com/repos/VoltDB/voltdb/commits{/sha}","git_commits_url":"https://api.github.com/repos/VoltDB/voltdb/git/commits{/sha}","comments_url":"https://api.github.com/repos/VoltDB/voltdb/comments{/number}","issue_comment_url":"https://api.github.com/repos/VoltDB/voltdb/issues/comments{/number}","contents_url":"https://api.github.com/repos/VoltDB/voltdb/contents/{+path}","compare_url":"https://api.github.com/repos/VoltDB/voltdb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/VoltDB/voltdb/merges","archive_url":"https://api.github.com/repos/VoltDB/voltdb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/VoltDB/voltdb/downloads","issues_url":"https://api.github.com/repos/VoltDB/voltdb/issues{/number}","pulls_url":"https://api.github.com/repos/VoltDB/voltdb/pulls{/number}","milestones_url":"https://api.github.com/repos/VoltDB/voltdb/milestones{/number}","notifications_url":"https://api.github.com/repos/VoltDB/voltdb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/VoltDB/voltdb/labels{/name}","releases_url":"https://api.github.com/repos/VoltDB/voltdb/releases{/id}","deployments_url":"https://api.github.com/repos/VoltDB/voltdb/deployments","created_at":"2011-09-23T18:46:12Z","updated_at":"2016-05-05T04:58:24Z","pushed_at":"2016-05-05T17:14:08Z","git_url":"git://github.com/VoltDB/voltdb.git","ssh_url":"git@github.com:VoltDB/voltdb.git","clone_url":"https://github.com/VoltDB/voltdb.git","svn_url":"https://github.com/VoltDB/voltdb","homepage":"http://community.voltdb.com/","size":626947,"stargazers_count":766,"watchers_count":766,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":235,"mirror_url":null,"open_issues_count":33,"forks":235,"open_issues":33,"watchers":766,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541"},"html":{"href":"https://github.com/VoltDB/voltdb/pull/3541"},"issue":{"href":"https://api.github.com/repos/VoltDB/voltdb/issues/3541"},"comments":{"href":"https://api.github.com/repos/VoltDB/voltdb/issues/3541/comments"},"review_comments":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541/comments"},"review_comment":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/VoltDB/voltdb/pulls/3541/commits"},"statuses":{"href":"https://api.github.com/repos/VoltDB/voltdb/statuses/8d521c48ea9ddce82cc97b906978e2e9203cd00c"}}}}