add change detection to file fetcher, cleanup code, fix spellings, swap example code to use file fetcher

master
Jaime Pillora 2017-08-24 20:24:47 +10:00
parent ef5fb99e14
commit 2e4decb22e
11 changed files with 92 additions and 74 deletions

View File

@ -3,17 +3,12 @@
#NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR
# ENSURE PORTS 5001,5002 ARE UNUSED
#http file server
go get github.com/jpillora/serve
serve --port 5002 --quiet . &
SERVEPID=$!
#initial build
go build -ldflags '-X main.BUILD_ID=1' -o myapp
go build -ldflags '-X main.BuildID=1' -o my_app
echo "BUILT APP (1)"
#run!
echo "RUNNING APP"
./myapp &
./my_app &
APPPID=$!
sleep 1
@ -24,7 +19,7 @@ sleep 1
#request during an update
curl localhost:5001?d=5s &
go build -ldflags '-X main.BUILD_ID=2' -o myappnew
go build -ldflags '-X main.BuildID=2' -o my_app_next
echo "BUILT APP (2)"
sleep 2
@ -35,7 +30,7 @@ sleep 1
#request during an update
curl localhost:5001?d=5s &
go build -ldflags '-X main.BUILD_ID=3' -o myappnew
go build -ldflags '-X main.BuildID=3' -o my_app_next
echo "BUILT APP (3)"
sleep 2
@ -48,28 +43,25 @@ curl localhost:5001
sleep 1
#end demo - cleanup
kill $SERVEPID
kill $APPPID
rm myapp* 2> /dev/null
rm my_app* 2> /dev/null
# Expected output:
# serving . on port 4000
# Expected output (hashes will vary across OS/arch/go-versions):
# BUILT APP (1)
# RUNNING APP
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) listening...
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello
# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) listening...
# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello
# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello
# BUILT APP (2)
# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) listening...
# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello
# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) exiting...
# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) listening...
# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello
# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello
# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello
# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) exiting...
# BUILT APP (3)
# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) listening...
# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello
# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello
# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello
# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) exiting...
# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello
# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) exiting...
# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) listening...
# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello
# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello
# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello
# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello
# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) exiting...

View File

@ -11,19 +11,20 @@ import (
//see example.sh for the use-case
var BUILD_ID = "0"
// BuildID is compile-time variable
var BuildID = "0"
//convert your 'main()' into a 'prog(state)'
//'prog()' is run in a child process
func prog(state overseer.State) {
fmt.Printf("app#%s (%s) listening...\n", BUILD_ID, state.ID)
fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID)
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
d, _ := time.ParseDuration(r.URL.Query().Get("d"))
time.Sleep(d)
fmt.Fprintf(w, "app#%s (%s) says hello\n", BUILD_ID, state.ID)
fmt.Fprintf(w, "app#%s (%s) says hello\n", BuildID, state.ID)
}))
http.Serve(state.Listener, nil)
fmt.Printf("app#%s (%s) exiting...\n", BUILD_ID, state.ID)
fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID)
}
//then create another 'main' which runs the upgrades
@ -32,10 +33,7 @@ func main() {
overseer.Run(overseer.Config{
Program: prog,
Address: ":5001",
Fetcher: &fetcher.HTTP{
URL: "http://localhost:5002/myappnew",
Interval: 1 * time.Second,
},
Debug: false, //display log of overseer actions
Fetcher: &fetcher.File{Path: "my_app_next"},
Debug: false, //display log of overseer actions
})
}

View File

@ -2,6 +2,7 @@ package fetcher
import "io"
// Interface defines the required fetcher functions
type Interface interface {
//Init should perform validation on fields. For
//example, ensure the appropriate URLs or keys
@ -12,12 +13,12 @@ type Interface interface {
//binary to fetch, and then stream it back the
//form of an io.Reader. If io.Reader is nil,
//then it is assumed there are no updates. Fetch
//will be run repeatly and forever. It is up the
//will be run repeatedly and forever. It is up the
//implementation to throttle the fetch frequency.
Fetch() (io.Reader, error)
}
//Converts a fetch function into interface
// Func converts a fetch function into the fetcher interface
func Func(fn func() (io.Reader, error)) Interface {
return &fetcher{fn}
}

View File

@ -1,82 +1,100 @@
package fetcher
import (
"errors"
"fmt"
"io"
"os"
"time"
)
// File is used to check new version
// File checks the provided Path, at the provided
// Interval for new Go binaries. When a new binary
// is found it will replace the currently running
// binary.
type File struct {
Path string
Interval time.Duration
// file modify time and its size makes up its hash
uniqHash string
delay bool
// hash is the file modify time and its size
hash string
delay bool
}
// Init interval and lastHash
// Init sets the Path and Interval options
func (f *File) Init() error {
if f.Path == "" {
return fmt.Errorf("Path required")
}
if f.Interval == 0 {
f.Interval = 10 * time.Second
if f.Interval < 1*time.Second {
f.Interval = 1 * time.Second
}
if err := f.updateHash(); err != nil {
return err
}
f.delay = false
return nil
}
// Fetch file
// Fetch file from the specified Path
func (f *File) Fetch() (io.Reader, error) {
//delay fetches after first
//only delay after first fetch
if f.delay {
time.Sleep(f.Interval)
}
f.delay = true
lastHash := f.uniqHash
lastHash := f.hash
if err := f.updateHash(); err != nil {
return nil, err
}
// no change
if lastHash == f.uniqHash {
if lastHash == f.hash {
return nil, nil
}
// changed!
file, err := os.Open(f.Path)
if err != nil {
return nil, err
}
//check every 1/4s for 5s to
//ensure its not mid-copy
const rate = 250 * time.Millisecond
const total = int(5 * time.Second / rate)
attempt := 1
for {
if attempt == total {
file.Close()
return nil, errors.New("file is currently being changed")
}
attempt++
//sleep
time.Sleep(rate)
//check hash!
if err := f.updateHash(); err != nil {
file.Close()
return nil, err
}
//check until no longer changing
if lastHash == f.hash {
break
}
lastHash = f.hash
}
return file, nil
}
func (f *File) updateHash() error {
file, err := os.Open(f.Path)
if err != nil {
// new version not exist, return
//binary does not exist, skip
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("Open file error: %s", err)
}
defer file.Close()
state, err := file.Stat()
s, err := file.Stat()
if err != nil {
return fmt.Errorf("Get file state error: %s", err)
return fmt.Errorf("Get file stat error: %s", err)
}
n := state.ModTime().UnixNano()
s := state.Size()
f.uniqHash = fmt.Sprintf("%d%d", n, s)
f.hash = fmt.Sprintf("%d|%d", s.ModTime().UnixNano(), s.Size())
return nil
}

View File

@ -25,7 +25,7 @@ type Github struct {
//By default a file will match if it contains
//both GOOS and GOARCH.
Asset func(filename string) bool
//interal state
//internal state
releaseURL string
delay bool
lastETag string
@ -42,6 +42,7 @@ func (h *Github) defaultAsset(filename string) bool {
return strings.Contains(filename, runtime.GOOS) && strings.Contains(filename, runtime.GOARCH)
}
// Init validates the provided config
func (h *Github) Init() error {
//apply defaults
if h.User == "" {
@ -62,6 +63,7 @@ func (h *Github) Init() error {
return nil
}
// Fetch the binary from the provided Repository
func (h *Github) Fetch() (io.Reader, error) {
//delay fetches after first
if h.delay {
@ -105,7 +107,7 @@ func (h *Github) Fetch() (io.Reader, error) {
return nil, fmt.Errorf("release location request failed (status code %d)", resp.StatusCode)
}
s3URL := resp.Header.Get("Location")
//psuedo-HEAD request
//pseudo-HEAD request
req, err = http.NewRequest("GET", s3URL, nil)
if err != nil {
return nil, fmt.Errorf("release location url error (%s)", err)

View File

@ -9,7 +9,7 @@ import (
"time"
)
//HTTPFetcher uses HEAD requests to poll the status of a given
//HTTP fetcher uses HEAD requests to poll the status of a given
//file. If it detects this file has been updated, it will fetch
//and return its io.Reader stream.
type HTTP struct {
@ -17,7 +17,7 @@ type HTTP struct {
URL string
Interval time.Duration
CheckHeaders []string
//interal state
//internal state
delay bool
lasts map[string]string
}
@ -25,6 +25,7 @@ type HTTP struct {
//if any of these change, the binary has been updated
var defaultHTTPCheckHeaders = []string{"ETag", "If-Modified-Since", "Last-Modified", "Content-Length"}
// Init validates the provided config
func (h *HTTP) Init() error {
//apply defaults
if h.URL == "" {
@ -40,6 +41,7 @@ func (h *HTTP) Init() error {
return nil
}
// Fetch the binary from the provided URL
func (h *HTTP) Fetch() (io.Reader, error) {
//delay fetches after first
if h.delay {

View File

@ -31,6 +31,7 @@ type S3 struct {
lastETag string
}
// Init validates the provided config
func (s *S3) Init() error {
if s.Bucket == "" {
return errors.New("S3 bucket not set")
@ -58,6 +59,7 @@ func (s *S3) Init() error {
return nil
}
// Fetch the binary from S3
func (s *S3) Fetch() (io.Reader, error) {
//delay fetches after first
if s.delay {

View File

@ -1,4 +1,5 @@
// Daemonizable self-upgrading binaries in Go (golang).
// Package overseer implements daemonizable
// self-upgrading binaries in Go (golang).
package overseer
import (
@ -22,6 +23,7 @@ const (
envBinCheckLegacy = "GO_UPGRADE_BIN_CHECK"
)
// Config defines overseer's run-time configuration
type Config struct {
//Required will prevent overseer from fallback to running
//running the program in the main process on failure.
@ -42,7 +44,7 @@ type Config struct {
//This helps to prevent unwieldy fetch.Interfaces from hogging
//too many resources. Defaults to 1 second.
MinFetchInterval time.Duration
//PreUpgrade runs after a binary has been retreived, user defined checks
//PreUpgrade runs after a binary has been retrieved, user defined checks
//can be run here and returning an error will cancel the upgrade.
PreUpgrade func(tempBinaryPath string) error
//Debug enables all [overseer] logs.
@ -91,7 +93,7 @@ func RunErr(c Config) error {
}
//Run executes overseer, if an error is
//encounted, overseer fallsback to running
//encountered, overseer fallsback to running
//the program directly (unless Required is set).
func Run(c Config) {
err := runErr(&c)

View File

@ -394,7 +394,7 @@ func (mp *master) fork() error {
}
mp.debugf("prog exited with %d", code)
//if a restarts are disabled or if it was an
//unexpected creash, proxy this exit straight
//unexpected crash, proxy this exit straight
//through to the main process
if mp.NoRestart || !mp.restarting {
os.Exit(code)

View File

@ -18,6 +18,7 @@ var (
DisabledState = State{Enabled: false}
)
// State contains the current run-time state of overseer
type State struct {
//whether overseer is running enabled. When enabled,
//this program will be running in a child process and

View File

@ -27,7 +27,7 @@ func move(dst, src string) error {
return nil
}
//HACK: we're shelling out to move because windows
//throws errors when crossing device boundaryes.
//throws errors when crossing device boundaries.
// https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/move.mspx?mfr=true
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/