-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from krallin/master
Initial PR
- Loading branch information
Showing
20 changed files
with
1,122 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
supercronic | ||
vendor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
sudo: false | ||
dist: trusty | ||
|
||
language: go | ||
|
||
go: "1.8" | ||
|
||
install: | ||
- curl https://glide.sh/get | sh | ||
- make deps | ||
- git clone https://github.com/sstephenson/bats.git --branch v0.4.0 --depth 1 "${HOME}/bats" | ||
- export "PATH=${PATH}:${HOME}/bats/bin" | ||
|
||
jobs: | ||
include: | ||
- stage: test | ||
script: | ||
- make fmt && make test | ||
- stage: build | ||
script: | ||
- mkdir -p dist | ||
- for arch in amd64 386 arm arm64; do GOARCH="$arch" go build && mv supercronic "dist/supercronic-${arch}"; done | ||
- cd dist | ||
- ls -lah * | ||
- file * | ||
- sha1sum * | ||
- sha256sum * | ||
deploy: | ||
provider: releases | ||
api_key: | ||
secure: "MPXVtn7XhcWkkDpDCcPqvfp1klsfGflsMrhBTH5WO2dUD352fJdbHTUi87Yuo+aFib+yNArzFn3cl9ZfUmD7vSSjmxqFjrgcIJT45Qqo4Y+T39JMUo7QuCsBUV4QbLlHMAyA3ZrcpWmpyGC5VgqiRN6D/XCTmB355fMfbF/Wov/shADiLLzYxDRkxUggx2nqBrG1Eo0JfS5Ji/MUbA5dhmOoDnf0YTsu2SWhS1nErj0HTe/j2/o7wG9aM1rQg6sU0DDTarPWNVJn6HNx0E65VzvfZH2v6g0rfLQ3sydeHdtvyS2KxBlCcp7ceJ2VHLuurR9IZTqH8GYA8GYAAZpo2oZF2esqH6tIpTIG5kJJy9Ybzw1o5Q3R3LAY18/IvdiUmfrMUy1Bkai1Lz1nHvcG5azwSOgrZ/hTbby1XPS4TdjbC7tyQXJ/u0ch+qxLOcIwKp/3DiE6nmMXJkCv4hf5YX/AYze2TKtm2uhE2qQF7kQ3tKi64nOBX9N3+mJVthS37JA8Zrak3D/5E4vtul87lahczOCNS2qYcET04Td77HJ1HEGgSnJETvnfG4+8LmLYoIqN3201Vsk585CNVpUY7PjYCxFBRadj7SfHmAq8mEQF7rpM8ELmBirkwta1QQq5Qma49ozCxWcnhnqw9NPRG3oNCrYsVC2APIrkvsOjVPo=" | ||
file: "dist/*" | ||
skip_cleanup: true | ||
on: | ||
tags: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2017, Aptible, Inc. | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
this software and associated documentation files (the "Software"), to deal in | ||
the Software without restriction, including without limitation the rights to | ||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
of the Software, and to permit persons to whom the Software is furnished to do | ||
so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*") | ||
SHELL=/bin/bash | ||
|
||
.PHONY: deps | ||
deps: | ||
glide install | ||
|
||
.PHONY: build | ||
build: $(GOFILES) | ||
go build | ||
|
||
.PHONY: unit | ||
unit: | ||
go test $$(go list ./... | grep -v /vendor/) | ||
go vet $$(go list ./... | grep -v /vendor/) | ||
|
||
.PHONY: integration | ||
integration: build | ||
bats integration | ||
|
||
.PHONY: test | ||
test: unit integration | ||
true | ||
|
||
.PHONY: fmt | ||
fmt: | ||
gofmt -l -w ${GOFILES_NOVENDOR} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
# Supercronic # | ||
|
||
Supercronic is a crontab-compatible job runner, designed specifically to run in | ||
containers. | ||
|
||
|
||
## Why Supercronic? ## | ||
|
||
Crontabs are the lingua franca of job scheduling, but typical server cron | ||
implementations are ill-suited for container environments: | ||
|
||
- They purge their environment before starting jobs. This is an important | ||
security feature in multi-user systems, but it breaks a fundamental | ||
configuration mechanism for containers. | ||
- They capture the output from the jobs they run, and often either want to | ||
email this output or simply discard it. In a containerized environment, | ||
logging task output and errors to `stdout` / `stderr` is often easier to work | ||
with. | ||
- They often don't respond gracefully to `SIGINT` / `SIGTERM`, and may leave | ||
running jobs orphaned when signaled. Again, this makes sense in a server | ||
environment where `init` will handle the orphan jobs and Cron isn't restarted | ||
often anyway, but it's inappropriate in a container environment as it'll | ||
result in jobs being forcefully terminated (i.e. `SIGKILL`'ed) when the | ||
container exits. | ||
- They often try to send their logs to syslog. This conveniently provides | ||
centralized logging when a syslog server is running, but with containers, | ||
simply logging to `stdout` or `stderr` is preferred. | ||
|
||
Finally, they are often very quiet, which makes the above issues difficult to | ||
understand or debug! | ||
|
||
The list could go on, but the fundamental takeaway is this: unlike typical | ||
server cron implementations, Supercronic tries very hard to do exactly what you | ||
expect from running `cron` in a container: | ||
|
||
- Your environment variables are available in jobs. | ||
- Job output is logged to `stdout` / `stderr`. | ||
- `SIGTERM` (or `SIGINT`, which you can deliver via CTRL+C when used | ||
interactively) triggers a graceful shutdown | ||
- Job return codes and schedules are also logged to `stdout` / `stderr`. | ||
|
||
## How does it work? ## | ||
|
||
- Install Supercronic (see below). | ||
- Point it at a crontab: `supercronic CRONTAB`. | ||
- You're done! | ||
|
||
|
||
### Installation | ||
|
||
- If you have a `go` toolchain available: `go install github.com/aptible/supercronic` | ||
- TODO: Docker installation instructions / packaging. | ||
|
||
|
||
## Crontab format ## | ||
|
||
Broadly speaking, Supercronic tries to process crontabs just like Vixie cron | ||
does. In most cases, it should be compatible with your existing crontab. | ||
|
||
There are, however, a few exceptions: | ||
|
||
- First, Supercronic supports second-resolution schedules: under the hood, | ||
Supercronic uses [the `cronexpr` package][cronexpr], so refer to its | ||
documentation to know exactly what you can do. | ||
- Second, Supercronic does not support changing users when running tasks. | ||
Again, this is something that hardly makes sense in a cron environment. This | ||
means that setting `USER` in your crontab won't have any effect. | ||
|
||
Here's an example crontab: | ||
|
||
``` | ||
# Run every minute | ||
*/1 * * * * echo "hello" | ||
# Run every 2 seconds | ||
*/2 * * * * * * ls 2>/dev/null | ||
``` | ||
|
||
|
||
## Environment variables ## | ||
|
||
Just like regular cron, Supercronic lets you specify environment variables in | ||
your crontab using a `KEY=VALUE` syntax. | ||
|
||
However, this is only here for compatibility with existing crontabs, and using | ||
this feature is generally **not recommended** when using Supercronic. | ||
|
||
Indeed, Supercronic does not wipe your environment before running jobs, so if | ||
you need environment variables to be available when your jobs run, just set | ||
them before starting Supercronic itself, and your jobs will inherit them | ||
(unless you've used cron before, this is exactly what you expect). | ||
|
||
For example, if you're using Docker, Supercronic | ||
|
||
|
||
## Logging ## | ||
|
||
Supercronic provides rich logging, and will let you know exactly what command | ||
triggered a given message. Here's an example: | ||
|
||
``` | ||
$ cat ./my-crontab | ||
*/5 * * * * * * echo "hello from Supercronic" | ||
$ ./supercronic ./my-crontab | ||
INFO[2017-07-10T19:40:44+02:00] read crontab: ./my-crontab | ||
INFO[2017-07-10T19:40:50+02:00] starting iteration=0 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
INFO[2017-07-10T19:40:50+02:00] hello from Supercronic channel=stdout iteration=0 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
INFO[2017-07-10T19:40:50+02:00] job succeeded iteration=0 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
INFO[2017-07-10T19:40:55+02:00] starting iteration=1 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
INFO[2017-07-10T19:40:55+02:00] hello from Supercronic channel=stdout iteration=1 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
INFO[2017-07-10T19:40:55+02:00] job succeeded iteration=1 job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
``` | ||
|
||
|
||
## Debugging ## | ||
|
||
If your jobs aren't running, or you'd simply like to double-check your crontab | ||
syntax, pass the `-debug` flag for more verbose logging: | ||
|
||
``` | ||
$ ./supercronic -debug ./my-crontab | ||
INFO[2017-07-10T19:43:51+02:00] read crontab: ./my-crontab | ||
DEBU[2017-07-10T19:43:51+02:00] try parse(7): */5 * * * * * * echo "hello from Supercronic"[0:15] = */5 * * * * * * | ||
DEBU[2017-07-10T19:43:51+02:00] job will run next at 2017-07-10 19:44:00 +0200 CEST job.command="echo "hello from Supercronic"" job.position=0 job.schedule="*/5 * * * * * *" | ||
``` | ||
|
||
|
||
## Duplicate Jobs ## | ||
|
||
Supercronic will wait for a given job to finish before that job is scheduled | ||
again (some cron implementations do this, others don't). If a job is falling | ||
behind schedule (i.e. it's taking too long to finish), Supercronic will warn | ||
you. | ||
|
||
Here is an example: | ||
|
||
``` | ||
# Sleep for 2 seconds every second. This will take too long. | ||
* * * * * * * sleep 2 | ||
$ ./supercronic ./my-crontab | ||
INFO[2017-07-11T12:24:25+02:00] read crontab: foo | ||
INFO[2017-07-11T12:24:27+02:00] starting iteration=0 job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
INFO[2017-07-11T12:24:29+02:00] job succeeded iteration=0 job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
WARN[2017-07-11T12:24:29+02:00] job took too long to run: it should have started 1.009438854s ago job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
INFO[2017-07-11T12:24:30+02:00] starting iteration=1 job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
INFO[2017-07-11T12:24:32+02:00] job succeeded iteration=1 job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
WARN[2017-07-11T12:24:32+02:00] job took too long to run: it should have started 1.014474099s ago job.command="sleep 2" job.position=0 job.schedule="* * * * * * *" | ||
``` | ||
|
||
[cronexpr]: https://github.com/gorhill/cronexpr |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package cron | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"github.com/aptible/supercronic/crontab" | ||
"github.com/sirupsen/logrus" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
"sync" | ||
"syscall" | ||
"time" | ||
) | ||
|
||
func startReaderDrain(wg *sync.WaitGroup, readerLogger *logrus.Entry, reader io.Reader) { | ||
wg.Add(1) | ||
|
||
go func() { | ||
defer wg.Done() | ||
|
||
scanner := bufio.NewScanner(reader) | ||
|
||
for scanner.Scan() { | ||
readerLogger.Info(scanner.Text()) | ||
} | ||
|
||
if err := scanner.Err(); err != nil { | ||
// The underlying reader might get closed by e.g. Wait(), or | ||
// even the process we're starting, so we don't log EOF-like | ||
// errors | ||
if strings.Contains(err.Error(), os.ErrClosed.Error()) { | ||
return | ||
} | ||
|
||
readerLogger.Error(err) | ||
} | ||
}() | ||
} | ||
|
||
func runJob(context *crontab.Context, command string, jobLogger *logrus.Entry) error { | ||
jobLogger.Info("starting") | ||
|
||
cmd := exec.Command(context.Shell, "-c", command) | ||
|
||
// Run in a separate process group so that in interactive usage, CTRL+C | ||
// stops supercronic, not the children threads. | ||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | ||
|
||
env := os.Environ() | ||
for k, v := range context.Environ { | ||
env = append(env, fmt.Sprintf("%s=%s", k, v)) | ||
} | ||
cmd.Env = env | ||
|
||
stdout, err := cmd.StdoutPipe() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
stderr, err := cmd.StderrPipe() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := cmd.Start(); err != nil { | ||
return err | ||
} | ||
|
||
var wg sync.WaitGroup | ||
|
||
stdoutLogger := jobLogger.WithFields(logrus.Fields{"channel": "stdout"}) | ||
startReaderDrain(&wg, stdoutLogger, stdout) | ||
|
||
stderrLogger := jobLogger.WithFields(logrus.Fields{"channel": "stderr"}) | ||
startReaderDrain(&wg, stderrLogger, stderr) | ||
|
||
wg.Wait() | ||
|
||
if err := cmd.Wait(); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func StartJob(wg *sync.WaitGroup, context *crontab.Context, job *crontab.Job, exitChan chan interface{}) { | ||
wg.Add(1) | ||
|
||
go func() { | ||
defer wg.Done() | ||
|
||
cronLogger := logrus.WithFields(logrus.Fields{ | ||
"job.schedule": job.Schedule, | ||
"job.command": job.Command, | ||
"job.position": job.Position, | ||
}) | ||
|
||
var cronIteration uint64 = 0 | ||
nextRun := job.Expression.Next(time.Now()) | ||
|
||
// NOTE: this (intentionally) does not run multiple instances of the | ||
// job concurrently | ||
for { | ||
nextRun = job.Expression.Next(nextRun) | ||
cronLogger.Debugf("job will run next at %v", nextRun) | ||
|
||
delay := nextRun.Sub(time.Now()) | ||
if delay < 0 { | ||
cronLogger.Warningf("job took too long to run: it should have started %v ago", -delay) | ||
nextRun = time.Now() | ||
continue | ||
} | ||
|
||
select { | ||
case <-exitChan: | ||
cronLogger.Debug("shutting down") | ||
return | ||
case <-time.After(delay): | ||
// Proceed normally | ||
} | ||
|
||
jobLogger := cronLogger.WithFields(logrus.Fields{ | ||
"iteration": cronIteration, | ||
}) | ||
|
||
err := runJob(context, job.Command, jobLogger) | ||
|
||
if err == nil { | ||
jobLogger.Info("job succeeded") | ||
} else { | ||
jobLogger.Error(err) | ||
} | ||
|
||
cronIteration++ | ||
} | ||
}() | ||
} |
Oops, something went wrong.