{"action":"created","comment":{"url":"https://api.github.com/repos/m3db/m3em/pulls/comments/119204803","pull_request_review_id":41063746,"id":119204803,"diff_hunk":"@@ -0,0 +1,825 @@\n+// Copyright (c) 2017 Uber Technologies, Inc.\n+//\n+// Permission is hereby granted, free of charge, to any person obtaining a copy\n+// of this software and associated documentation files (the \"Software\"), to deal\n+// in the Software without restriction, including without limitation the rights\n+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+// copies of the Software, and to permit persons to whom the Software is\n+// furnished to do so, subject to the following conditions:\n+//\n+// The above copyright notice and this permission notice shall be included in\n+// all copies or substantial portions of the Software.\n+//\n+// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n+// THE SOFTWARE.\n+\n+package agent\n+\n+import (\n+\t\"fmt\"\n+\t\"hash/crc32\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"os\"\n+\t\"path\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"sync/atomic\"\n+\t\"time\"\n+\n+\thb \"github.com/m3db/m3em/generated/proto/heartbeat\"\n+\t\"github.com/m3db/m3em/generated/proto/m3em\"\n+\t\"github.com/m3db/m3em/os/exec\"\n+\t\"github.com/m3db/m3em/os/fs\"\n+\n+\t\"path/filepath\"\n+\n+\txclock \"github.com/m3db/m3x/clock\"\n+\txerrors \"github.com/m3db/m3x/errors\"\n+\t\"github.com/m3db/m3x/instrument\"\n+\txlog \"github.com/m3db/m3x/log\"\n+\t\"github.com/uber-go/tally\"\n+\tcontext \"golang.org/x/net/context\"\n+\t\"google.golang.org/grpc\"\n+\t\"google.golang.org/grpc/codes\"\n+)\n+\n+const (\n+\tdefaultReportInterval       = 5 * time.Second\n+\tdefaultHeartbeatConnTimeout = time.Minute\n+\tdefaultTestCanaryPrefix     = \"test-canary-file\"\n+)\n+\n+var (\n+\terrProcessMonitorNotDefined     = fmt.Errorf(\"process monitor not defined\")\n+\terrTeardownHeartbeat            = fmt.Errorf(\"remote agent received Teardown(), turning off heartbeating\")\n+\terrSetupInitializeHostResources = fmt.Errorf(\"unable to initialize host resources, turning off heartbeating\")\n+\terrHeartbeatSendingTimedout     = fmt.Errorf(\"heartbeat sending timed out, remote agent reset\")\n+\terrNoValidTargetsSpecified      = fmt.Errorf(\"no valid target destinations specified\")\n+\terrOnlyDataFileMultiTarget      = fmt.Errorf(\"multiple targets are only supported for data files\")\n+)\n+\n+type opAgent struct {\n+\tsync.RWMutex\n+\ttoken               string\n+\texecutablePath      string\n+\tconfigPath          string\n+\tnewProcessMonitorFn newProcessMonitorFn\n+\tprocessMonitor      exec.ProcessMonitor\n+\theartbeater         *opAgentHeartBeater\n+\n+\trunning            int32\n+\tstopping           int32\n+\theartbeatTimeoutCh chan struct{}\n+\n+\topts    Options\n+\tlogger  xlog.Logger\n+\tmetrics *opAgentMetrics\n+\tdoneCh  chan struct{}\n+\tcloseCh chan struct{}\n+}\n+\n+type newProcessMonitorFn func(exec.Cmd, exec.ProcessListener) (exec.ProcessMonitor, error)\n+\n+// New creates and retuns a new Operator Agent\n+func New(\n+\topts Options,\n+) (Agent, error) {\n+\tif err := opts.Validate(); err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tif err := canaryWriteTest(opts.WorkingDirectory()); err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tagent := &opAgent{\n+\t\topts:                opts,\n+\t\tlogger:              opts.InstrumentOptions().Logger(),\n+\t\tmetrics:             newAgentMetrics(opts.InstrumentOptions().MetricsScope()),\n+\t\tnewProcessMonitorFn: exec.NewProcessMonitor,\n+\t\tdoneCh:              make(chan struct{}),\n+\t\tcloseCh:             make(chan struct{}),\n+\t}\n+\tgo agent.reportMetrics()\n+\treturn agent, nil\n+}\n+\n+func (o *opAgent) Close() error {\n+\to.closeCh <- struct{}{}\n+\t<-o.doneCh\n+\treturn nil\n+}\n+\n+func canaryWriteTest(dir string) error {\n+\tfi, err := os.Stat(dir)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"unable to stat directory, [ err = %v ]\", err)\n+\t}\n+\tif !fi.IsDir() {\n+\t\treturn fmt.Errorf(\"path is not a directory\")\n+\t}\n+\n+\tfd, err := ioutil.TempFile(dir, defaultTestCanaryPrefix)\n+\tif err != nil {\n+\t\treturn fmt.Errorf(\"unable to create canary file, [ err = %v ]\", err)\n+\t}\n+\tos.Remove(fd.Name())\n+\n+\treturn nil\n+}\n+\n+func updateBoolGauge(b bool, m tally.Gauge) {\n+\tif b {\n+\t\tm.Update(1)\n+\t} else {\n+\t\tm.Update(0)\n+\t}\n+}\n+\n+func (o *opAgent) reportMetrics() {\n+\treportTicker := time.NewTicker(defaultReportInterval)\n+\tfor {\n+\t\tselect {\n+\t\tcase <-reportTicker.C:\n+\t\t\trunning, exec, conf := o.state()\n+\t\t\tupdateBoolGauge(running, o.metrics.running)\n+\t\t\tupdateBoolGauge(exec != \"\", o.metrics.execTransferred)\n+\t\t\tupdateBoolGauge(conf != \"\", o.metrics.confTransferred)\n+\t\tcase <-o.closeCh:\n+\t\t\treportTicker.Stop()\n+\t\t\to.doneCh <- struct{}{}\n+\t\t\treturn\n+\t\t}\n+\t}\n+}\n+\n+func (o *opAgent) Running() bool {\n+\treturn atomic.LoadInt32(&o.running) == 1\n+}\n+\n+func (o *opAgent) state() (bool, string, string) {\n+\to.RLock()\n+\tdefer o.RUnlock()\n+\treturn o.Running(), o.executablePath, o.configPath\n+}\n+\n+func (o *opAgent) Start(ctx context.Context, request *m3em.StartRequest) (*m3em.StartResponse, error) {\n+\to.logger.Infof(\"received Start()\")\n+\to.Lock()\n+\tdefer o.Unlock()\n+\n+\tif o.Running() {\n+\t\treturn nil, grpc.Errorf(codes.FailedPrecondition, \"already running\")\n+\t}\n+\n+\tif o.executablePath == \"\" {\n+\t\treturn nil, grpc.Errorf(codes.FailedPrecondition, \"agent missing build\")\n+\t}\n+\n+\tif o.configPath == \"\" {\n+\t\treturn nil, grpc.Errorf(codes.FailedPrecondition, \"agent missing config\")\n+\t}\n+\n+\tif err := o.startWithLock(); err != nil {\n+\t\treturn nil, grpc.Errorf(codes.Internal, \"unable to start: %v\", err)\n+\t}\n+\n+\treturn &m3em.StartResponse{}, nil\n+}\n+\n+func (o *opAgent) newListenerFn() func(error) {\n+\treturn func(err error) {\n+\t\tif err == nil {\n+\t\t\terr = fmt.Errorf(\"test process terminated without error\")\n+\t\t} else {\n+\t\t\terr = fmt.Errorf(\"test process terminated with error: %v\", err)\n+\t\t}\n+\t\to.logger.Warnf(\"%v\", err)\n+\t\tif stopping := atomic.LoadInt32(&o.stopping); stopping == 0 && o.heartbeater != nil {\n+\t\t\to.heartbeater.notifyProcessTermination(err)\n+\t\t}\n+\t\tatomic.StoreInt32(&o.running, 0)\n+\t}\n+}\n+\n+func (o *opAgent) newProcessListener() exec.ProcessListener {\n+\tvar (\n+\t\tlistenFn   = o.newListenerFn()\n+\t\tonComplete = func() { listenFn(nil) }\n+\t\tonError    = func(err error) { listenFn(err) }\n+\t)\n+\treturn exec.NewProcessListener(onComplete, onError)\n+}\n+\n+func (o *opAgent) startWithLock() error {\n+\tvar (\n+\t\tpath, args = o.opts.ExecGenFn()(o.executablePath, o.configPath)\n+\t\tosArgs     = append([]string{path}, args...)\n+\t\tcmd        = exec.Cmd{\n+\t\t\tPath:      path,\n+\t\t\tArgs:      osArgs,\n+\t\t\tOutputDir: o.opts.WorkingDirectory(),\n+\t\t\tEnv:       o.opts.EnvMap(),\n+\t\t}\n+\t\tlistener = o.newProcessListener()\n+\t)\n+\tpm, err := o.newProcessMonitorFn(cmd, listener)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\to.logger.Infof(\"executing command: %+v\", cmd)\n+\tif err := pm.Start(); err != nil {\n+\t\treturn err\n+\t}\n+\tatomic.StoreInt32(&o.running, 1)\n+\to.processMonitor = pm\n+\treturn nil\n+}\n+\n+func (o *opAgent) Stop(ctx context.Context, request *m3em.StopRequest) (*m3em.StopResponse, error) {\n+\to.logger.Infof(\"received Stop()\")\n+\to.Lock()\n+\tdefer o.Unlock()\n+\n+\tif !o.Running() {\n+\t\treturn nil, grpc.Errorf(codes.FailedPrecondition, \"not running\")\n+\t}\n+\n+\tatomic.StoreInt32(&o.stopping, 1)\n+\tif err := o.stopWithLock(); err != nil {\n+\t\treturn nil, grpc.Errorf(codes.Internal, \"unable to stop: %v\", err)\n+\t}\n+\tatomic.StoreInt32(&o.stopping, 0)\n+\n+\treturn &m3em.StopResponse{}, nil\n+}\n+\n+func (o *opAgent) stopWithLock() error {\n+\tif o.processMonitor == nil {\n+\t\treturn errProcessMonitorNotDefined\n+\t}\n+\n+\tif err := o.processMonitor.Stop(); err != nil {\n+\t\treturn err\n+\t}\n+\n+\to.processMonitor = nil\n+\tatomic.StoreInt32(&o.running, 0)\n+\treturn nil\n+}\n+\n+func (o *opAgent) resetWithLock(heartbeatNotification error) error {\n+\tvar multiErr xerrors.MultiError\n+\n+\tif o.heartbeater != nil {\n+\t\to.logger.Infof(\"stopping heartbeating\")\n+\t\tif heartbeatNotification != nil {\n+\t\t\to.heartbeater.notifyOverwrite(heartbeatNotification)\n+\t\t}\n+\t\tmultiErr = multiErr.Add(o.heartbeater.close())\n+\t\to.heartbeater = nil\n+\t}\n+\n+\tif o.heartbeatTimeoutCh != nil {\n+\t\tclose(o.heartbeatTimeoutCh)\n+\t\to.heartbeatTimeoutCh = nil\n+\t}\n+\n+\tif o.Running() {\n+\t\to.logger.Infof(\"process running, stopping\")\n+\t\tif err := o.stopWithLock(); err != nil {\n+\t\t\to.logger.Warnf(\"unable to stop: %v\", err)\n+\t\t\tmultiErr = multiErr.Add(err)\n+\t\t}\n+\t}\n+\n+\to.logger.Infof(\"releasing host resources\")\n+\tif err := o.opts.ReleaseHostResourcesFn()(); err != nil {\n+\t\to.logger.Infof(\"unable to release host resources: %v\", err)\n+\t\tmultiErr = multiErr.Add(err)\n+\t}\n+\n+\to.token = \"\"\n+\to.executablePath = \"\"\n+\to.configPath = \"\"\n+\tatomic.StoreInt32(&o.running, 0)\n+\n+\treturn multiErr.FinalError()\n+}\n+\n+func (o *opAgent) Teardown(ctx context.Context, request *m3em.TeardownRequest) (*m3em.TeardownResponse, error) {\n+\to.logger.Infof(\"received Teardown()\")\n+\to.Lock()\n+\tdefer o.Unlock()\n+\n+\tif err := o.resetWithLock(errTeardownHeartbeat); err != nil {\n+\t\treturn nil, grpc.Errorf(codes.Internal, \"unable to teardown: %v\", err)\n+\t}\n+\n+\treturn &m3em.TeardownResponse{}, nil\n+}\n+\n+func (o *opAgent) isSetup() bool {\n+\to.RLock()\n+\tdefer o.RUnlock()\n+\treturn o.isSetupWithLock()\n+}\n+\n+func (o *opAgent) isSetupWithLock() bool {\n+\treturn o.token != \"\"\n+}\n+\n+func (o *opAgent) Setup(ctx context.Context, request *m3em.SetupRequest) (*m3em.SetupResponse, error) {\n+\to.logger.Infof(\"receieved Setup()\")\n+\n+\t// nil check\n+\tif request == nil || request.SessionToken == \"\" {\n+\t\treturn nil, grpc.Errorf(codes.InvalidArgument, \"nil request\")\n+\t}\n+\n+\to.Lock()\n+\tdefer o.Unlock()\n+\n+\tif o.token != \"\" && o.token != request.SessionToken && !request.Force {\n+\t\treturn nil, grpc.Errorf(codes.AlreadyExists, \"agent already initialized with token: %s\", o.token)\n+\t}\n+\n+\tif o.isSetupWithLock() {\n+\t\t// reset agent\n+\t\tmsg := fmt.Errorf(\"heartbeating being overwritten by new setup request: %+v\", *request)\n+\t\tif err := o.resetWithLock(msg); err != nil {\n+\t\t\treturn nil, grpc.Errorf(codes.Aborted, \"unable to reset: %v\", err)\n+\t\t}\n+\t}\n+\n+\t// remove any files stored in the working directory\n+\twd := o.opts.WorkingDirectory()\n+\to.logger.Infof(\"removing contents from working directory: %s\", wd)\n+\tif err := fs.RemoveContents(wd); err != nil {\n+\t\treturn nil, grpc.Errorf(codes.Internal, \"unable to clear working directory: %v\", err)\n+\t}\n+\n+\t// initialize any resources needed on the host\n+\to.logger.Infof(\"initializing host resources\")\n+\tif err := o.opts.InitHostResourcesFn()(); err != nil {\n+\t\to.resetWithLock(errSetupInitializeHostResources) // release any resources\n+\t\treturn nil, grpc.Errorf(codes.Internal, \"unable to initialize host resources: %v\", err)\n+\t}\n+\n+\t// setup new heartbeating\n+\tif request.HeartbeatEnabled {\n+\t\to.heartbeatTimeoutCh = make(chan struct{})\n+\t\tbeater, err := newHeartbeater(o, o.heartbeatTimeoutCh, request.OperatorUuid, request.HeartbeatEndpoint, o.opts.InstrumentOptions())\n+\t\tif err != nil {\n+\t\t\to.resetWithLock(errSetupInitializeHostResources) // release any resources\n+\t\t\treturn nil, grpc.Errorf(codes.Aborted, \"unable to start heartbeating process: %v\", err)\n+\t\t}\n+\t\to.heartbeater = beater\n+\t\tgo o.monitorHeartbeatTimeout(o.heartbeatTimeoutCh)\n+\t\to.heartbeater.start(time.Second * time.Duration(request.HeartbeatFrequencySecs))\n+\t}\n+\n+\to.token = request.SessionToken\n+\treturn &m3em.SetupResponse{}, nil\n+}\n+\n+func (o *opAgent) monitorHeartbeatTimeout(timeoutCh chan struct{}) {\n+\tfor range timeoutCh {\n+\t\to.logger.Warnf(\"heartbeat sending timed out, resetting agent\")\n+\t\to.Lock()\n+\t\terr := o.resetWithLock(nil) // nil indicates we don't want to send a heartbeat\n+\t\to.Unlock()\n+\t\tif err == nil {\n+\t\t\to.logger.Infof(\"successfully reset agent\")\n+\t\t} else {\n+\t\t\to.logger.Warnf(\"error while resetting agent: %v\", err)\n+\t\t}\n+\t\t// resetting breaks out of the loop as it closes the timeoutCh\n+\t}\n+}\n+\n+// non-thread safe multi-writer\n+type multiWriter struct {\n+\tio.Closer\n+\tfds []*os.File\n+}\n+\n+// based on signature of os.OpenFile\n+func newMulitWriter(paths []string, flags int, fileMode os.FileMode, dirMode os.FileMode) (*multiWriter, error) {\n+\tmw := &multiWriter{}\n+\tfor _, p := range paths {\n+\t\t// ensure directory exists\n+\t\tdir := filepath.Dir(p)\n+\t\terr := os.MkdirAll(dir, dirMode)\n+\t\tif err != nil {\n+\t\t\tmw.Close() // cleanup\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\t// create file\n+\t\tfd, err := os.OpenFile(p, flags, fileMode)\n+\t\tif err != nil {\n+\t\t\tmw.Close() // cleanup\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tmw.fds = append(mw.fds, fd)\n+\t}\n+\treturn mw, nil\n+}\n+\n+// mimics signature of os.File.Write\n+func (mw *multiWriter) write(b []byte) (int, error) {\n+\tvar (\n+\t\tfirst = true\n+\t\tn     int\n+\t\terr   error\n+\t)\n+\n+\t// write the provided data to all fds, terminating\n+\t// early if any fd write fails or is not the same\n+\t// as the others\n+\tfor _, fd := range mw.fds {\n+\t\t// initialize for the first fd\n+\t\tif first {\n+\t\t\tn, err = fd.Write(b)\n+\t\t\tfirst = false\n+\t\t\tif err != nil {\n+\t\t\t\treturn n, err\n+\t\t\t}\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\t// for the rest of the fds, ensure\n+\t\t// they all write the same number of bytes\n+\t\t// as the first one","path":"agent/agent.go","position":null,"original_position":460,"commit_id":"5bba9ed9e6b3d872990178b5285183f7a19386e9","original_commit_id":"626e0374ab5535889e15a87470761985a2cdd90b","user":{"login":"prateek","id":199982,"avatar_url":"https://avatars0.githubusercontent.com/u/199982?v=3","gravatar_id":"","url":"https://api.github.com/users/prateek","html_url":"https://github.com/prateek","followers_url":"https://api.github.com/users/prateek/followers","following_url":"https://api.github.com/users/prateek/following{/other_user}","gists_url":"https://api.github.com/users/prateek/gists{/gist_id}","starred_url":"https://api.github.com/users/prateek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/prateek/subscriptions","organizations_url":"https://api.github.com/users/prateek/orgs","repos_url":"https://api.github.com/users/prateek/repos","events_url":"https://api.github.com/users/prateek/events{/privacy}","received_events_url":"https://api.github.com/users/prateek/received_events","type":"User","site_admin":false},"body":"done","created_at":"2017-05-30T20:23:01Z","updated_at":"2017-05-30T20:23:01Z","html_url":"https://github.com/m3db/m3em/pull/1#discussion_r119204803","pull_request_url":"https://api.github.com/repos/m3db/m3em/pulls/1","_links":{"self":{"href":"https://api.github.com/repos/m3db/m3em/pulls/comments/119204803"},"html":{"href":"https://github.com/m3db/m3em/pull/1#discussion_r119204803"},"pull_request":{"href":"https://api.github.com/repos/m3db/m3em/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/m3db/m3em/pulls/1","id":113098733,"html_url":"https://github.com/m3db/m3em/pull/1","diff_url":"https://github.com/m3db/m3em/pull/1.diff","patch_url":"https://github.com/m3db/m3em/pull/1.patch","issue_url":"https://api.github.com/repos/m3db/m3em/issues/1","number":1,"state":"open","locked":false,"title":"Initial commit","user":{"login":"prateek","id":199982,"avatar_url":"https://avatars0.githubusercontent.com/u/199982?v=3","gravatar_id":"","url":"https://api.github.com/users/prateek","html_url":"https://github.com/prateek","followers_url":"https://api.github.com/users/prateek/followers","following_url":"https://api.github.com/users/prateek/following{/other_user}","gists_url":"https://api.github.com/users/prateek/gists{/gist_id}","starred_url":"https://api.github.com/users/prateek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/prateek/subscriptions","organizations_url":"https://api.github.com/users/prateek/orgs","repos_url":"https://api.github.com/users/prateek/repos","events_url":"https://api.github.com/users/prateek/events{/privacy}","received_events_url":"https://api.github.com/users/prateek/received_events","type":"User","site_admin":false},"body":"This PR is ready for review.\r\n/cc @xichen2020 @cw9 @ben-lerner @martin-mao @robskillington \r\n\r\nHere are the changes I'm planning on making already:\r\n- [x] Merge Operator & Instance\r\n- [x] Make M3DBInstance general (rename Instance to Node), put any bits specific to M3DB in a separate package\r\n- [x] Simplify Cluster interface\r\n~- [ ] Ability to specify 'MustFn' in the options, i.e. minimise boilerplate in error handling~ going to put it this in a test harness layer above this codebase\r\n","created_at":"2017-03-29T01:04:43Z","updated_at":"2017-05-30T20:23:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c9f9968f25532a8d4434c555bc0556bf26d7a9c9","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/m3db/m3em/pulls/1/commits","review_comments_url":"https://api.github.com/repos/m3db/m3em/pulls/1/comments","review_comment_url":"https://api.github.com/repos/m3db/m3em/pulls/comments{/number}","comments_url":"https://api.github.com/repos/m3db/m3em/issues/1/comments","statuses_url":"https://api.github.com/repos/m3db/m3em/statuses/5bba9ed9e6b3d872990178b5285183f7a19386e9","head":{"label":"m3db:prateek/initial-commit","ref":"prateek/initial-commit","sha":"5bba9ed9e6b3d872990178b5285183f7a19386e9","user":{"login":"m3db","id":19937171,"avatar_url":"https://avatars3.githubusercontent.com/u/19937171?v=3","gravatar_id":"","url":"https://api.github.com/users/m3db","html_url":"https://github.com/m3db","followers_url":"https://api.github.com/users/m3db/followers","following_url":"https://api.github.com/users/m3db/following{/other_user}","gists_url":"https://api.github.com/users/m3db/gists{/gist_id}","starred_url":"https://api.github.com/users/m3db/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m3db/subscriptions","organizations_url":"https://api.github.com/users/m3db/orgs","repos_url":"https://api.github.com/users/m3db/repos","events_url":"https://api.github.com/users/m3db/events{/privacy}","received_events_url":"https://api.github.com/users/m3db/received_events","type":"Organization","site_admin":false},"repo":{"id":86521791,"name":"m3em","full_name":"m3db/m3em","owner":{"login":"m3db","id":19937171,"avatar_url":"https://avatars3.githubusercontent.com/u/19937171?v=3","gravatar_id":"","url":"https://api.github.com/users/m3db","html_url":"https://github.com/m3db","followers_url":"https://api.github.com/users/m3db/followers","following_url":"https://api.github.com/users/m3db/following{/other_user}","gists_url":"https://api.github.com/users/m3db/gists{/gist_id}","starred_url":"https://api.github.com/users/m3db/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m3db/subscriptions","organizations_url":"https://api.github.com/users/m3db/orgs","repos_url":"https://api.github.com/users/m3db/repos","events_url":"https://api.github.com/users/m3db/events{/privacy}","received_events_url":"https://api.github.com/users/m3db/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/m3db/m3em","description":"A cluster environment manager","fork":false,"url":"https://api.github.com/repos/m3db/m3em","forks_url":"https://api.github.com/repos/m3db/m3em/forks","keys_url":"https://api.github.com/repos/m3db/m3em/keys{/key_id}","collaborators_url":"https://api.github.com/repos/m3db/m3em/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/m3db/m3em/teams","hooks_url":"https://api.github.com/repos/m3db/m3em/hooks","issue_events_url":"https://api.github.com/repos/m3db/m3em/issues/events{/number}","events_url":"https://api.github.com/repos/m3db/m3em/events","assignees_url":"https://api.github.com/repos/m3db/m3em/assignees{/user}","branches_url":"https://api.github.com/repos/m3db/m3em/branches{/branch}","tags_url":"https://api.github.com/repos/m3db/m3em/tags","blobs_url":"https://api.github.com/repos/m3db/m3em/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/m3db/m3em/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/m3db/m3em/git/refs{/sha}","trees_url":"https://api.github.com/repos/m3db/m3em/git/trees{/sha}","statuses_url":"https://api.github.com/repos/m3db/m3em/statuses/{sha}","languages_url":"https://api.github.com/repos/m3db/m3em/languages","stargazers_url":"https://api.github.com/repos/m3db/m3em/stargazers","contributors_url":"https://api.github.com/repos/m3db/m3em/contributors","subscribers_url":"https://api.github.com/repos/m3db/m3em/subscribers","subscription_url":"https://api.github.com/repos/m3db/m3em/subscription","commits_url":"https://api.github.com/repos/m3db/m3em/commits{/sha}","git_commits_url":"https://api.github.com/repos/m3db/m3em/git/commits{/sha}","comments_url":"https://api.github.com/repos/m3db/m3em/comments{/number}","issue_comment_url":"https://api.github.com/repos/m3db/m3em/issues/comments{/number}","contents_url":"https://api.github.com/repos/m3db/m3em/contents/{+path}","compare_url":"https://api.github.com/repos/m3db/m3em/compare/{base}...{head}","merges_url":"https://api.github.com/repos/m3db/m3em/merges","archive_url":"https://api.github.com/repos/m3db/m3em/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/m3db/m3em/downloads","issues_url":"https://api.github.com/repos/m3db/m3em/issues{/number}","pulls_url":"https://api.github.com/repos/m3db/m3em/pulls{/number}","milestones_url":"https://api.github.com/repos/m3db/m3em/milestones{/number}","notifications_url":"https://api.github.com/repos/m3db/m3em/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/m3db/m3em/labels{/name}","releases_url":"https://api.github.com/repos/m3db/m3em/releases{/id}","deployments_url":"https://api.github.com/repos/m3db/m3em/deployments","created_at":"2017-03-29T00:48:48Z","updated_at":"2017-05-29T22:22:12Z","pushed_at":"2017-05-30T20:22:16Z","git_url":"git://github.com/m3db/m3em.git","ssh_url":"git@github.com:m3db/m3em.git","clone_url":"https://github.com/m3db/m3em.git","svn_url":"https://github.com/m3db/m3em","homepage":"","size":321,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"m3db:master","ref":"master","sha":"814fda9986992a1c2cfac65014e7356abf1b3312","user":{"login":"m3db","id":19937171,"avatar_url":"https://avatars3.githubusercontent.com/u/19937171?v=3","gravatar_id":"","url":"https://api.github.com/users/m3db","html_url":"https://github.com/m3db","followers_url":"https://api.github.com/users/m3db/followers","following_url":"https://api.github.com/users/m3db/following{/other_user}","gists_url":"https://api.github.com/users/m3db/gists{/gist_id}","starred_url":"https://api.github.com/users/m3db/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m3db/subscriptions","organizations_url":"https://api.github.com/users/m3db/orgs","repos_url":"https://api.github.com/users/m3db/repos","events_url":"https://api.github.com/users/m3db/events{/privacy}","received_events_url":"https://api.github.com/users/m3db/received_events","type":"Organization","site_admin":false},"repo":{"id":86521791,"name":"m3em","full_name":"m3db/m3em","owner":{"login":"m3db","id":19937171,"avatar_url":"https://avatars3.githubusercontent.com/u/19937171?v=3","gravatar_id":"","url":"https://api.github.com/users/m3db","html_url":"https://github.com/m3db","followers_url":"https://api.github.com/users/m3db/followers","following_url":"https://api.github.com/users/m3db/following{/other_user}","gists_url":"https://api.github.com/users/m3db/gists{/gist_id}","starred_url":"https://api.github.com/users/m3db/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m3db/subscriptions","organizations_url":"https://api.github.com/users/m3db/orgs","repos_url":"https://api.github.com/users/m3db/repos","events_url":"https://api.github.com/users/m3db/events{/privacy}","received_events_url":"https://api.github.com/users/m3db/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/m3db/m3em","description":"A cluster environment manager","fork":false,"url":"https://api.github.com/repos/m3db/m3em","forks_url":"https://api.github.com/repos/m3db/m3em/forks","keys_url":"https://api.github.com/repos/m3db/m3em/keys{/key_id}","collaborators_url":"https://api.github.com/repos/m3db/m3em/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/m3db/m3em/teams","hooks_url":"https://api.github.com/repos/m3db/m3em/hooks","issue_events_url":"https://api.github.com/repos/m3db/m3em/issues/events{/number}","events_url":"https://api.github.com/repos/m3db/m3em/events","assignees_url":"https://api.github.com/repos/m3db/m3em/assignees{/user}","branches_url":"https://api.github.com/repos/m3db/m3em/branches{/branch}","tags_url":"https://api.github.com/repos/m3db/m3em/tags","blobs_url":"https://api.github.com/repos/m3db/m3em/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/m3db/m3em/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/m3db/m3em/git/refs{/sha}","trees_url":"https://api.github.com/repos/m3db/m3em/git/trees{/sha}","statuses_url":"https://api.github.com/repos/m3db/m3em/statuses/{sha}","languages_url":"https://api.github.com/repos/m3db/m3em/languages","stargazers_url":"https://api.github.com/repos/m3db/m3em/stargazers","contributors_url":"https://api.github.com/repos/m3db/m3em/contributors","subscribers_url":"https://api.github.com/repos/m3db/m3em/subscribers","subscription_url":"https://api.github.com/repos/m3db/m3em/subscription","commits_url":"https://api.github.com/repos/m3db/m3em/commits{/sha}","git_commits_url":"https://api.github.com/repos/m3db/m3em/git/commits{/sha}","comments_url":"https://api.github.com/repos/m3db/m3em/comments{/number}","issue_comment_url":"https://api.github.com/repos/m3db/m3em/issues/comments{/number}","contents_url":"https://api.github.com/repos/m3db/m3em/contents/{+path}","compare_url":"https://api.github.com/repos/m3db/m3em/compare/{base}...{head}","merges_url":"https://api.github.com/repos/m3db/m3em/merges","archive_url":"https://api.github.com/repos/m3db/m3em/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/m3db/m3em/downloads","issues_url":"https://api.github.com/repos/m3db/m3em/issues{/number}","pulls_url":"https://api.github.com/repos/m3db/m3em/pulls{/number}","milestones_url":"https://api.github.com/repos/m3db/m3em/milestones{/number}","notifications_url":"https://api.github.com/repos/m3db/m3em/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/m3db/m3em/labels{/name}","releases_url":"https://api.github.com/repos/m3db/m3em/releases{/id}","deployments_url":"https://api.github.com/repos/m3db/m3em/deployments","created_at":"2017-03-29T00:48:48Z","updated_at":"2017-05-29T22:22:12Z","pushed_at":"2017-05-30T20:22:16Z","git_url":"git://github.com/m3db/m3em.git","ssh_url":"git@github.com:m3db/m3em.git","clone_url":"https://github.com/m3db/m3em.git","svn_url":"https://github.com/m3db/m3em","homepage":"","size":321,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/m3db/m3em/pulls/1"},"html":{"href":"https://github.com/m3db/m3em/pull/1"},"issue":{"href":"https://api.github.com/repos/m3db/m3em/issues/1"},"comments":{"href":"https://api.github.com/repos/m3db/m3em/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/m3db/m3em/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/m3db/m3em/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/m3db/m3em/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/m3db/m3em/statuses/5bba9ed9e6b3d872990178b5285183f7a19386e9"}}}}
{"actor":{"display_login":"prateek"}}