{"action":"created","comment":{"url":"https://api.github.com/repos/lytics/metafora/pulls/comments/29393831","id":29393831,"diff_hunk":"@@ -0,0 +1,316 @@\n+package statemachine\n+\n+import (\n+\t\"fmt\"\n+\t\"time\"\n+\n+\t\"github.com/lytics/metafora\"\n+)\n+\n+type StateCode string\n+\n+const (\n+\tRunnable  StateCode = \"runnable\"  // Scheduled\n+\tSleeping            = \"sleeping\"  // Scheduled, not running until time has elapsed\n+\tCompleted           = \"completed\" // Terminal, not scheduled\n+\tKilled              = \"killed\"    // Terminal, not scheduled\n+\tFailed              = \"failed\"    // Terminal, not scheduled\n+\tFault               = \"fault\"     // Scheduled, in error handling / retry logic\n+\tPaused              = \"paused\"    // Scheduled, not running\n+)\n+\n+// Terminal states will never run and cannot transition to a non-terminal\n+// state.\n+func (s StateCode) Terminal() bool {\n+\tswitch s {\n+\tcase Runnable, Sleeping, Paused, Fault:\n+\t\treturn false\n+\tcase Completed, Killed, Failed:\n+\t\treturn true\n+\tdefault:\n+\t\tpanic(\"unknown state \" + s)\n+\t}\n+}\n+\n+// State represents the current state of a stateful handler. See StateCode for\n+// details. Until and Errors are extra state used by the Sleeping and Fault\n+// states respectively.\n+type State struct {\n+\tCode   StateCode  `json:\"state\"`\n+\tUntil  *time.Time `json:\"until,omitempty\"`\n+\tErrors []Err      `json:\"errors,omitempty\"`\n+}\n+\n+func (s State) String() string {\n+\tswitch s.Code {\n+\tcase Sleeping:\n+\t\treturn fmt.Sprintf(\"%s until %s\", s.Code, s.Until)\n+\tdefault:\n+\t\treturn string(s.Code)\n+\t}\n+}\n+\n+// Messages are events that cause state transitions. Until and Err are used by\n+// the Sleep and Error messages respectively.\n+type Message struct {\n+\tCode MessageCode `json:\"message\"`\n+\n+\t// Until is when the statemachine should transition from sleeping to runnable\n+\tUntil *time.Time `json:\"until,omitempty\"`\n+\n+\t// Err is the error that caused this Error message\n+\tErr error `json:\"error,omitempty\"`\n+}\n+\n+// MessageCode is the symbolic name of a state transition.\n+type MessageCode string\n+\n+const (\n+\tRun        MessageCode = \"run\"\n+\tSleep                  = \"sleep\"\n+\tPause                  = \"pause\"\n+\tKill                   = \"kill\"\n+\tError                  = \"error\"\n+\tComplete               = \"complete\"\n+\tCheckpoint             = \"checkpoint\"\n+\n+\t// Special event which triggers state machine to exit without transitioning\n+\t// between states.\n+\tRelease MessageCode = \"release\"\n+)\n+\n+// Transitions represent a state machine transition from one state to another\n+// given an event message.\n+type Transition struct {\n+\tEvent MessageCode\n+\tFrom  StateCode\n+\tTo    StateCode\n+}\n+\n+func (t Transition) String() string {\n+\treturn fmt.Sprintf(\"%v---%v--->%v\", t.From, t.Event, t.To)\n+}\n+\n+var (\n+\t// Rules is the state transition table.\n+\tRules = []Transition{\n+\t\t// Runnable can transition to anything\n+\t\t{Event: Checkpoint, From: Runnable, To: Runnable},\n+\t\t{Event: Release, From: Runnable, To: Runnable},\n+\t\t{Event: Sleep, From: Runnable, To: Sleeping},\n+\t\t{Event: Complete, From: Runnable, To: Completed},\n+\t\t{Event: Kill, From: Runnable, To: Killed},\n+\t\t{Event: Error, From: Runnable, To: Fault},\n+\t\t{Event: Pause, From: Runnable, To: Paused},\n+\n+\t\t// Sleeping can return to Runnable or be Killed/Paused\n+\t\t{Event: Checkpoint, From: Sleeping, To: Sleeping},\n+\t\t{Event: Sleep, From: Sleeping, To: Sleeping},\n+\t\t{Event: Run, From: Sleeping, To: Runnable},\n+\t\t{Event: Kill, From: Sleeping, To: Killed},\n+\t\t{Event: Pause, From: Sleeping, To: Paused},\n+\n+\t\t// The error state transitions to either sleeping, failed, or released (to\n+\t\t// allow custom error handlers to workaround localitly related errors).\n+\t\t{Event: Sleep, From: Fault, To: Sleeping},\n+\t\t{Event: Error, From: Fault, To: Failed},\n+\n+\t\t// Paused can return to Runnable, be put to Sleep, or Killed\n+\t\t{Event: Checkpoint, From: Paused, To: Paused},\n+\t\t{Event: Release, From: Paused, To: Paused},\n+\t\t{Event: Run, From: Paused, To: Runnable},\n+\t\t{Event: Sleep, From: Paused, To: Sleeping},\n+\t\t{Event: Kill, From: Paused, To: Killed},\n+\n+\t\t// Completed, Failed, and Killed are terminal states that cannot transition\n+\t\t// to anything.\n+\t}\n+)\n+\n+// StatefulHandler is the function signature that the state machine is able to\n+// run. Instead of metafora.Handler's Stop method, StatefulHandlers receive\n+// Messages via the commands chan and return their exit status via a Message.\n+//\n+// Normally StatefulHandlers simply return a Message as soon as it's received\n+// on the commands chan. However, it's also acceptable for a handler to return\n+// a different Message. For example if it encounters an error during shutdown,\n+// it may choose to return that error as an Error Message as opposed to the\n+// original command.\n+type StatefulHandler func(taskID string, commands <-chan Message) Message","path":"statemachine/statemachine.go","position":139,"original_position":139,"commit_id":"9f40a328d842b58ec5ec66b5b6e88584b5491b70","original_commit_id":"9f40a328d842b58ec5ec66b5b6e88584b5491b70","user":{"login":"schmichael","id":113362,"avatar_url":"https://avatars.githubusercontent.com/u/113362?v=3","gravatar_id":"","url":"https://api.github.com/users/schmichael","html_url":"https://github.com/schmichael","followers_url":"https://api.github.com/users/schmichael/followers","following_url":"https://api.github.com/users/schmichael/following{/other_user}","gists_url":"https://api.github.com/users/schmichael/gists{/gist_id}","starred_url":"https://api.github.com/users/schmichael/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schmichael/subscriptions","organizations_url":"https://api.github.com/users/schmichael/orgs","repos_url":"https://api.github.com/users/schmichael/repos","events_url":"https://api.github.com/users/schmichael/events{/privacy}","received_events_url":"https://api.github.com/users/schmichael/received_events","type":"User","site_admin":false},"body":"This could also be a single method interface, but a straight up func type just seemed slightly simpler.","created_at":"2015-04-29T23:59:14Z","updated_at":"2015-04-29T23:59:14Z","html_url":"https://github.com/lytics/metafora/pull/112#discussion_r29393831","pull_request_url":"https://api.github.com/repos/lytics/metafora/pulls/112","_links":{"self":{"href":"https://api.github.com/repos/lytics/metafora/pulls/comments/29393831"},"html":{"href":"https://github.com/lytics/metafora/pull/112#discussion_r29393831"},"pull_request":{"href":"https://api.github.com/repos/lytics/metafora/pulls/112"}}},"pull_request":{"url":"https://api.github.com/repos/lytics/metafora/pulls/112","id":34417248,"html_url":"https://github.com/lytics/metafora/pull/112","diff_url":"https://github.com/lytics/metafora/pull/112.diff","patch_url":"https://github.com/lytics/metafora/pull/112.patch","issue_url":"https://api.github.com/repos/lytics/metafora/issues/112","number":112,"state":"open","locked":false,"title":"Metafora State Machine","user":{"login":"schmichael","id":113362,"avatar_url":"https://avatars.githubusercontent.com/u/113362?v=3","gravatar_id":"","url":"https://api.github.com/users/schmichael","html_url":"https://github.com/schmichael","followers_url":"https://api.github.com/users/schmichael/followers","following_url":"https://api.github.com/users/schmichael/following{/other_user}","gists_url":"https://api.github.com/users/schmichael/gists{/gist_id}","starred_url":"https://api.github.com/users/schmichael/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schmichael/subscriptions","organizations_url":"https://api.github.com/users/schmichael/orgs","repos_url":"https://api.github.com/users/schmichael/repos","events_url":"https://api.github.com/users/schmichael/events{/privacy}","received_events_url":"https://api.github.com/users/schmichael/received_events","type":"User","site_admin":false},"body":"**Not Mergeable** Lots more tests to write/fix.\r\n\r\nPort of Lytics's internal state machine code to Metafora. Includes sending task level commands through etcd. Does *not* include a `StateStore` implementation for saving state since our internal method is pretty specific to our infrastructure.\r\n\r\nSee **README.md** for a (hopefully) good overview of the statemachine.\r\n\r\nOther changes:\r\n\r\n* Handler.Run no longer gets the task ID; the HandlerFunc does.\r\n * HandlerFuncs setup the state shared by Run and Stop methods because Run and Stop are called concurrently, so it makes sense to provide the task ID as early as possible. This was necessary to allow the HandlerFunc to create a task specific command listener for the state machine to use.\r\n\r\n(Sorry for some really bad commit messages. I planned on squashing them but never got around to it.)","created_at":"2015-04-29T23:48:41Z","updated_at":"2015-04-29T23:59:14Z","closed_at":null,"merged_at":null,"merge_commit_sha":"097a9c46402b24545618d3cfa347ad7fb67ccf8d","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/lytics/metafora/pulls/112/commits","review_comments_url":"https://api.github.com/repos/lytics/metafora/pulls/112/comments","review_comment_url":"https://api.github.com/repos/lytics/metafora/pulls/comments{/number}","comments_url":"https://api.github.com/repos/lytics/metafora/issues/112/comments","statuses_url":"https://api.github.com/repos/lytics/metafora/statuses/9f40a328d842b58ec5ec66b5b6e88584b5491b70","head":{"label":"lytics:statemachine-2","ref":"statemachine-2","sha":"9f40a328d842b58ec5ec66b5b6e88584b5491b70","user":{"login":"lytics","id":1951824,"avatar_url":"https://avatars.githubusercontent.com/u/1951824?v=3","gravatar_id":"","url":"https://api.github.com/users/lytics","html_url":"https://github.com/lytics","followers_url":"https://api.github.com/users/lytics/followers","following_url":"https://api.github.com/users/lytics/following{/other_user}","gists_url":"https://api.github.com/users/lytics/gists{/gist_id}","starred_url":"https://api.github.com/users/lytics/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lytics/subscriptions","organizations_url":"https://api.github.com/users/lytics/orgs","repos_url":"https://api.github.com/users/lytics/repos","events_url":"https://api.github.com/users/lytics/events{/privacy}","received_events_url":"https://api.github.com/users/lytics/received_events","type":"Organization","site_admin":false},"repo":{"id":24692885,"name":"metafora","full_name":"lytics/metafora","owner":{"login":"lytics","id":1951824,"avatar_url":"https://avatars.githubusercontent.com/u/1951824?v=3","gravatar_id":"","url":"https://api.github.com/users/lytics","html_url":"https://github.com/lytics","followers_url":"https://api.github.com/users/lytics/followers","following_url":"https://api.github.com/users/lytics/following{/other_user}","gists_url":"https://api.github.com/users/lytics/gists{/gist_id}","starred_url":"https://api.github.com/users/lytics/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lytics/subscriptions","organizations_url":"https://api.github.com/users/lytics/orgs","repos_url":"https://api.github.com/users/lytics/repos","events_url":"https://api.github.com/users/lytics/events{/privacy}","received_events_url":"https://api.github.com/users/lytics/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lytics/metafora","description":"Ordasity inspired distributed task runner.","fork":false,"url":"https://api.github.com/repos/lytics/metafora","forks_url":"https://api.github.com/repos/lytics/metafora/forks","keys_url":"https://api.github.com/repos/lytics/metafora/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lytics/metafora/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lytics/metafora/teams","hooks_url":"https://api.github.com/repos/lytics/metafora/hooks","issue_events_url":"https://api.github.com/repos/lytics/metafora/issues/events{/number}","events_url":"https://api.github.com/repos/lytics/metafora/events","assignees_url":"https://api.github.com/repos/lytics/metafora/assignees{/user}","branches_url":"https://api.github.com/repos/lytics/metafora/branches{/branch}","tags_url":"https://api.github.com/repos/lytics/metafora/tags","blobs_url":"https://api.github.com/repos/lytics/metafora/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lytics/metafora/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lytics/metafora/git/refs{/sha}","trees_url":"https://api.github.com/repos/lytics/metafora/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lytics/metafora/statuses/{sha}","languages_url":"https://api.github.com/repos/lytics/metafora/languages","stargazers_url":"https://api.github.com/repos/lytics/metafora/stargazers","contributors_url":"https://api.github.com/repos/lytics/metafora/contributors","subscribers_url":"https://api.github.com/repos/lytics/metafora/subscribers","subscription_url":"https://api.github.com/repos/lytics/metafora/subscription","commits_url":"https://api.github.com/repos/lytics/metafora/commits{/sha}","git_commits_url":"https://api.github.com/repos/lytics/metafora/git/commits{/sha}","comments_url":"https://api.github.com/repos/lytics/metafora/comments{/number}","issue_comment_url":"https://api.github.com/repos/lytics/metafora/issues/comments{/number}","contents_url":"https://api.github.com/repos/lytics/metafora/contents/{+path}","compare_url":"https://api.github.com/repos/lytics/metafora/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lytics/metafora/merges","archive_url":"https://api.github.com/repos/lytics/metafora/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lytics/metafora/downloads","issues_url":"https://api.github.com/repos/lytics/metafora/issues{/number}","pulls_url":"https://api.github.com/repos/lytics/metafora/pulls{/number}","milestones_url":"https://api.github.com/repos/lytics/metafora/milestones{/number}","notifications_url":"https://api.github.com/repos/lytics/metafora/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lytics/metafora/labels{/name}","releases_url":"https://api.github.com/repos/lytics/metafora/releases{/id}","created_at":"2014-10-01T19:48:52Z","updated_at":"2015-04-28T21:08:46Z","pushed_at":"2015-04-29T23:48:41Z","git_url":"git://github.com/lytics/metafora.git","ssh_url":"git@github.com:lytics/metafora.git","clone_url":"https://github.com/lytics/metafora.git","svn_url":"https://github.com/lytics/metafora","homepage":"https://godoc.org/github.com/lytics/metafora","size":1493,"stargazers_count":29,"watchers_count":29,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":23,"forks":2,"open_issues":23,"watchers":29,"default_branch":"master"}},"base":{"label":"lytics:master","ref":"master","sha":"30331e68ad6365118da33169d28fe53316160751","user":{"login":"lytics","id":1951824,"avatar_url":"https://avatars.githubusercontent.com/u/1951824?v=3","gravatar_id":"","url":"https://api.github.com/users/lytics","html_url":"https://github.com/lytics","followers_url":"https://api.github.com/users/lytics/followers","following_url":"https://api.github.com/users/lytics/following{/other_user}","gists_url":"https://api.github.com/users/lytics/gists{/gist_id}","starred_url":"https://api.github.com/users/lytics/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lytics/subscriptions","organizations_url":"https://api.github.com/users/lytics/orgs","repos_url":"https://api.github.com/users/lytics/repos","events_url":"https://api.github.com/users/lytics/events{/privacy}","received_events_url":"https://api.github.com/users/lytics/received_events","type":"Organization","site_admin":false},"repo":{"id":24692885,"name":"metafora","full_name":"lytics/metafora","owner":{"login":"lytics","id":1951824,"avatar_url":"https://avatars.githubusercontent.com/u/1951824?v=3","gravatar_id":"","url":"https://api.github.com/users/lytics","html_url":"https://github.com/lytics","followers_url":"https://api.github.com/users/lytics/followers","following_url":"https://api.github.com/users/lytics/following{/other_user}","gists_url":"https://api.github.com/users/lytics/gists{/gist_id}","starred_url":"https://api.github.com/users/lytics/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lytics/subscriptions","organizations_url":"https://api.github.com/users/lytics/orgs","repos_url":"https://api.github.com/users/lytics/repos","events_url":"https://api.github.com/users/lytics/events{/privacy}","received_events_url":"https://api.github.com/users/lytics/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lytics/metafora","description":"Ordasity inspired distributed task runner.","fork":false,"url":"https://api.github.com/repos/lytics/metafora","forks_url":"https://api.github.com/repos/lytics/metafora/forks","keys_url":"https://api.github.com/repos/lytics/metafora/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lytics/metafora/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lytics/metafora/teams","hooks_url":"https://api.github.com/repos/lytics/metafora/hooks","issue_events_url":"https://api.github.com/repos/lytics/metafora/issues/events{/number}","events_url":"https://api.github.com/repos/lytics/metafora/events","assignees_url":"https://api.github.com/repos/lytics/metafora/assignees{/user}","branches_url":"https://api.github.com/repos/lytics/metafora/branches{/branch}","tags_url":"https://api.github.com/repos/lytics/metafora/tags","blobs_url":"https://api.github.com/repos/lytics/metafora/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lytics/metafora/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lytics/metafora/git/refs{/sha}","trees_url":"https://api.github.com/repos/lytics/metafora/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lytics/metafora/statuses/{sha}","languages_url":"https://api.github.com/repos/lytics/metafora/languages","stargazers_url":"https://api.github.com/repos/lytics/metafora/stargazers","contributors_url":"https://api.github.com/repos/lytics/metafora/contributors","subscribers_url":"https://api.github.com/repos/lytics/metafora/subscribers","subscription_url":"https://api.github.com/repos/lytics/metafora/subscription","commits_url":"https://api.github.com/repos/lytics/metafora/commits{/sha}","git_commits_url":"https://api.github.com/repos/lytics/metafora/git/commits{/sha}","comments_url":"https://api.github.com/repos/lytics/metafora/comments{/number}","issue_comment_url":"https://api.github.com/repos/lytics/metafora/issues/comments{/number}","contents_url":"https://api.github.com/repos/lytics/metafora/contents/{+path}","compare_url":"https://api.github.com/repos/lytics/metafora/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lytics/metafora/merges","archive_url":"https://api.github.com/repos/lytics/metafora/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lytics/metafora/downloads","issues_url":"https://api.github.com/repos/lytics/metafora/issues{/number}","pulls_url":"https://api.github.com/repos/lytics/metafora/pulls{/number}","milestones_url":"https://api.github.com/repos/lytics/metafora/milestones{/number}","notifications_url":"https://api.github.com/repos/lytics/metafora/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lytics/metafora/labels{/name}","releases_url":"https://api.github.com/repos/lytics/metafora/releases{/id}","created_at":"2014-10-01T19:48:52Z","updated_at":"2015-04-28T21:08:46Z","pushed_at":"2015-04-29T23:48:41Z","git_url":"git://github.com/lytics/metafora.git","ssh_url":"git@github.com:lytics/metafora.git","clone_url":"https://github.com/lytics/metafora.git","svn_url":"https://github.com/lytics/metafora","homepage":"https://godoc.org/github.com/lytics/metafora","size":1493,"stargazers_count":29,"watchers_count":29,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":23,"forks":2,"open_issues":23,"watchers":29,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/lytics/metafora/pulls/112"},"html":{"href":"https://github.com/lytics/metafora/pull/112"},"issue":{"href":"https://api.github.com/repos/lytics/metafora/issues/112"},"comments":{"href":"https://api.github.com/repos/lytics/metafora/issues/112/comments"},"review_comments":{"href":"https://api.github.com/repos/lytics/metafora/pulls/112/comments"},"review_comment":{"href":"https://api.github.com/repos/lytics/metafora/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/lytics/metafora/pulls/112/commits"},"statuses":{"href":"https://api.github.com/repos/lytics/metafora/statuses/9f40a328d842b58ec5ec66b5b6e88584b5491b70"}}}}