-
Notifications
You must be signed in to change notification settings - Fork 0
First draft #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
First draft #1
Changes from all commits
de3b342
973fa07
6169164
3fba61a
7988f5f
d4efb44
78b60d7
69de5bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# vi:set tabstop=4 shiftwidth=4 noexpandtab: | ||
all: deps build run | ||
|
||
deps: | ||
test -d vendor/github.com/op/go-logging || git clone --depth 1 [email protected]:op/go-logging vendor/github.com/op/go-logging | ||
|
||
build: | ||
go build -o bin/counter | ||
|
||
run: | ||
go run main.go | ||
|
||
clean: | ||
rm -f bin/counter | ||
rm -f data/counter.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// vi:set tabstop=8 shiftwidth=8 noexpandtab: | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"flag" | ||
"os" | ||
|
||
"github.com/op/go-logging" | ||
|
||
"github.com/nocive/go-request-counter/src/counter" | ||
"github.com/nocive/go-request-counter/src/storage" | ||
"github.com/nocive/go-request-counter/src/request" | ||
) | ||
|
||
var logger = logging.MustGetLogger(counter.LoggerPrefix) | ||
|
||
var format = logging.MustStringFormatter( | ||
`%{color}• %{shortfunc} %{level:.4s} %{id:03x}%{color:reset} ‣ %{message}`, | ||
) | ||
|
||
var ( | ||
bindAddr string | ||
dataPath string | ||
maxClients int = counter.MaxClients | ||
requestTtl string | ||
refreshInterval string = counter.RefreshInterval | ||
saveInterval string = counter.SaveInterval | ||
sleepPerRequest string = counter.SleepPerRequest | ||
|
||
displayHelp bool | ||
|
||
cfg *counter.Config | ||
cnt *counter.RequestCounter | ||
bck *request.RequestBucket | ||
stg *storage.RequestCounterStorage | ||
) | ||
|
||
func main() { | ||
logging.SetFormatter(format) | ||
|
||
flag.StringVar(&bindAddr, "bind", counter.DefaultBindAddr, "which address to bind to in the form of addr:port.") | ||
flag.StringVar(&requestTtl, "ttl", counter.DefaultRequestTtl, "request ttl expressed as a time duration string (eg: 30s for 30 seconds).") | ||
flag.StringVar(&dataPath, "path", counter.DefaultDataPath, "path to the storage filename.") | ||
flag.BoolVar(&displayHelp, "help", false, "display this help text.") | ||
flag.Parse() | ||
|
||
if displayHelp { | ||
fmt.Printf("Usage: %s [-bind address] [-ttl ttl] [-path path]\n\n", os.Args[0]) | ||
flag.PrintDefaults() | ||
os.Exit(0) | ||
} | ||
|
||
cfg, err := counter.NewConfig( | ||
bindAddr, | ||
dataPath, | ||
maxClients, | ||
requestTtl, | ||
refreshInterval, | ||
saveInterval, | ||
sleepPerRequest, | ||
) | ||
if err != nil { | ||
logger.Fatal(err) | ||
} | ||
|
||
bck = request.NewRequestBucket(cfg.RequestTtl) | ||
stg = storage.NewRequestCounterStorage(cfg.DataPath) | ||
cnt = counter.NewRequestCounter(*cfg, *bck, *stg, *logger) | ||
|
||
if err := cnt.Init(); err != nil { | ||
logger.Fatal(err) | ||
} | ||
cnt.Start() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// vi:set tabstop=8 shiftwidth=8 noexpandtab: | ||
package counter | ||
|
||
import "time" | ||
|
||
const ( | ||
// default path + filename where to store the data in | ||
DefaultDataPath = "./data/counter.json" | ||
|
||
// default bind address to listen to | ||
DefaultBindAddr = "0.0.0.0:6666" | ||
|
||
// default request time to live (valid string for time.ParseDuration) | ||
DefaultRequestTtl = "60s" | ||
|
||
// max concurrent clients allowed | ||
MaxClients = 5 | ||
|
||
// how often should the counter data be refreshed (string for time.ParseDuration) | ||
RefreshInterval = "1s" | ||
|
||
// how often should the counter data be saved to disk (string for time.ParseDuration) | ||
SaveInterval = "90s" | ||
|
||
// for how long should each request sleep | ||
SleepPerRequest = "2s" | ||
|
||
// prefix used for the log messages | ||
LoggerPrefix = "go-request-counter" | ||
) | ||
|
||
type Config struct { | ||
BindAddr string | ||
DataPath string | ||
MaxClients int | ||
RequestTtl time.Duration | ||
RefreshInterval time.Duration | ||
SaveInterval time.Duration | ||
SleepPerRequest time.Duration | ||
} | ||
|
||
func NewConfig( | ||
bindAddr string, | ||
dataPath string, | ||
maxClients int, | ||
requestTtl string, | ||
refreshInterval string, | ||
saveInterval string, | ||
sleepPerRequest string, | ||
) (*Config, error) { | ||
_requestTtl, err := time.ParseDuration(requestTtl) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_refreshInterval, err := time.ParseDuration(refreshInterval) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_saveInterval, err := time.ParseDuration(saveInterval) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_sleepPerRequest, err := time.ParseDuration(sleepPerRequest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &Config{ | ||
BindAddr: bindAddr, | ||
DataPath: dataPath, | ||
MaxClients: maxClients, | ||
RequestTtl: _requestTtl, | ||
RefreshInterval: _refreshInterval, | ||
SaveInterval: _saveInterval, | ||
SleepPerRequest: _sleepPerRequest, | ||
}, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// vi:set tabstop=8 shiftwidth=8 noexpandtab: | ||
package counter | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/op/go-logging" | ||
|
||
"github.com/nocive/go-request-counter/src/storage" | ||
"github.com/nocive/go-request-counter/src/request" | ||
) | ||
|
||
type RequestCounter struct { | ||
config Config | ||
bucket request.RequestBucket | ||
storage storage.RequestCounterStorage | ||
logger logging.Logger | ||
} | ||
|
||
func NewRequestCounter(c Config, b request.RequestBucket, s storage.RequestCounterStorage, l logging.Logger) *RequestCounter { | ||
return &RequestCounter{ | ||
config: c, | ||
bucket: b, | ||
storage: s, | ||
logger: l, | ||
} | ||
} | ||
|
||
func (r *RequestCounter) Init() error { | ||
var err error | ||
|
||
r.logger.Infof("booting!") | ||
|
||
if !r.storage.Exists() { | ||
r.logger.Info("data file doesn't exist, creating") | ||
if err = r.storage.Create(); err != nil { | ||
return err | ||
} | ||
} else { | ||
r.logger.Info("data file exists, loading") | ||
if err = r.storage.Load(&r.bucket); err != nil { | ||
return err | ||
} | ||
|
||
c := r.bucket.GetCount() | ||
r.logger.Infof("%d requests loaded from data file", c) | ||
|
||
if (c > 0) { | ||
r.logger.Info("firing a manual refresh") | ||
c := r.bucket.Refresh() // purge expired events before starting | ||
r.logger.Infof("purged %d requests from bucket", c) | ||
} | ||
} | ||
|
||
r.logger.Infof( | ||
"[ttl %s] [refresh %s] [save %s] [sleep %s] [count %d]", | ||
r.config.RequestTtl, | ||
r.config.RefreshInterval, | ||
r.config.SaveInterval, | ||
r.config.SleepPerRequest, | ||
r.bucket.GetCount(), | ||
) | ||
|
||
r.logger.Info("initializing tickers and signal traps") | ||
r.traps() | ||
|
||
return nil | ||
} | ||
|
||
func (r *RequestCounter) Start() { | ||
sema := make(chan struct{}, r.config.MaxClients) | ||
|
||
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) { | ||
sema <- struct{}{} | ||
defer func() { <-sema }() | ||
r.process(response, request) | ||
}) | ||
|
||
r.logger.Infof("preparing to listen on %s", r.config.BindAddr) | ||
http.ListenAndServe(r.config.BindAddr, nil) | ||
} | ||
|
||
func (r *RequestCounter) process(response http.ResponseWriter, request *http.Request) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we start to go by the book now, this should implement the Then the semaphore should become a reusable http middleware. Then create |
||
clientIP := request.Header.Get("X-Client-IP") | ||
if clientIP == "" { | ||
clientIP = request.RemoteAddr | ||
clientIP, _, _ = net.SplitHostPort(clientIP) | ||
} | ||
r.logger.Infof("request received [ip %s]", clientIP) | ||
|
||
r.bucket.AddNow(clientIP) | ||
currentCount := r.bucket.GetCount() | ||
|
||
io.WriteString(response, fmt.Sprintf("%d\n", currentCount)) | ||
r.logger.Infof("counter incremented [count %d]", currentCount) | ||
|
||
time.Sleep(r.config.SleepPerRequest) | ||
r.logger.Info("request finished") | ||
} | ||
|
||
func (r *RequestCounter) shutdown() error { | ||
r.logger.Info("saving data to file") | ||
err := r.storage.Save(&r.bucket) | ||
return err | ||
} | ||
|
||
func (r *RequestCounter) traps() { | ||
quit := make(chan struct{}) | ||
|
||
refreshTicker := time.NewTicker(r.config.RefreshInterval) | ||
saveTicker := time.NewTicker(r.config.SaveInterval) | ||
|
||
c := make(chan os.Signal, 2) | ||
signal.Notify(c, os.Interrupt, syscall.SIGTERM) | ||
go func() { | ||
<-c | ||
r.logger.Info("caught signal, cleaning up...") | ||
refreshTicker.Stop() | ||
saveTicker.Stop() | ||
if err := r.shutdown(); err != nil { | ||
r.logger.Fatal(err) | ||
} | ||
os.Exit(0) | ||
}() | ||
|
||
go func() { | ||
for { | ||
select { | ||
case <- refreshTicker.C: | ||
if c := r.bucket.GetCount(); c > 0 { | ||
r.logger.Infof("refresh [count %d]", c) | ||
r.bucket.Refresh() | ||
} else { | ||
r.logger.Info("-- idling --") | ||
} | ||
|
||
case <- saveTicker.C: | ||
r.logger.Info("snapshotting data to file") | ||
r.storage.Save(&r.bucket) | ||
|
||
case <- quit: | ||
refreshTicker.Stop() | ||
saveTicker.Stop() | ||
return | ||
} | ||
} | ||
}() | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
never seen the underscore naming convention used in Go