Skip to content

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
data/*
bin/*
vendor/*
!.gitkeep

*.swp

# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
Expand Down
15 changes: 15 additions & 0 deletions Makefile
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
Empty file added bin/.gitkeep
Empty file.
Empty file added data/.gitkeep
Empty file.
75 changes: 75 additions & 0 deletions main.go
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()
}
77 changes: 77 additions & 0 deletions src/counter/config.go
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)

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

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
}
156 changes: 156 additions & 0 deletions src/counter/counter.go
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) {

Choose a reason for hiding this comment

The 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 http.Handler interface
https://golang.org/pkg/net/http/#Handler

Then the semaphore should become a reusable http middleware.
http://www.alexedwards.net/blog/making-and-using-middleware

Then create http.Server in the main function and .ListenAndServe() from there.

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
}
}
}()
}

Loading