Skip to content

Commit d3070dc

Browse files
authored
Merge pull request #149 from github/meiji163/skip-host
Skip hosts API
2 parents f987bf1 + 1c27790 commit d3070dc

File tree

10 files changed

+220
-5
lines changed

10 files changed

+220
-5
lines changed

build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ function precheck() {
7272
ok=1
7373
fi
7474

75-
if ! go version | egrep -q 'go(1\.1[234])' ; then
76-
echo "go version must be 1.12 or above"
75+
if ! go version | egrep -q 'go(1\.1[6789])' ; then
76+
echo "go version must be 1.16 or above"
7777
ok=1
7878
fi
7979

doc/http.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ Notes:
102102

103103
- `/check-read-if-exists/<app>/<store-type>/<store-name>/<threshold>`: like `/check-read`, but if the metric is unknown (e.g. `<store-name>` not in `freno`'s configuration), return `200 OK`. This is useful for hybrid systems where some metrics need to be strictly controlled, and some not. `freno` would probe the important stores, and still can serve requests for all stores.
104104

105+
- `/skip-host/<hostname>/ttl/<ttl-minutes>`: skip host when aggregating metrics for specified number of minutes. If host is already skipped, update the TTL.
106+
- `/skip-host/<hostname>`: same as `/skip-host/<hostname>/ttl/60`
107+
- `/recover-host/<hostname>`: recover a previously skipped host.
108+
- `/skipped-hosts`: list currently skipped hosts
109+
105110
### Other requests
106111

107112
- `/help`: show all supported request paths

doc/mysql-backend.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ CREATE TABLE throttled_apps (
8282
ratio DOUBLE,
8383
PRIMARY KEY (app_name)
8484
);
85+
86+
CREATE TABLE skipped_hosts (
87+
host_name varchar(128) NOT NULL,
88+
skipped_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
89+
expires_at TIMESTAMP NOT NULL,
90+
91+
PRIMARY KEY (host_name)
92+
);
8593
```
8694

8795
The `BackendMySQLUser` account must have `SELECT, INSERT, DELETE, UPDATE` privileges on those tables.

pkg/group/consensus_service.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ type ConsensusService interface {
2828
UnthrottleApp(appName string) error
2929
RecentAppsMap() (result map[string](*base.RecentApp))
3030

31+
SkipHost(hostName string, ttlMinutes int64, expireAt time.Time) error
32+
RecoverHost(hostName string) error
33+
SkippedHostsMap() (result map[string]time.Time)
34+
3135
IsHealthy() bool
3236
IsLeader() bool
3337
GetLeader() string

pkg/group/fsm.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func (f *fsm) Apply(l *raft.Log) interface{} {
2626
return f.applyThrottleApp(c.Key, c.ExpireAt, c.Ratio)
2727
case "unthrottle":
2828
return f.applyUnthrottleApp(c.Key)
29+
case "skip":
30+
return f.applySkipHost(c.Key, c.ExpireAt)
31+
case "recover":
32+
return f.applyRecoverHost(c.Key)
2933
}
3034
return log.Errorf("unrecognized command operation: %s", c.Operation)
3135
}
@@ -38,6 +42,11 @@ func (f *fsm) Snapshot() (raft.FSMSnapshot, error) {
3842
for appName, appThrottle := range f.throttler.ThrottledAppsMap() {
3943
snapshot.data.throttledApps[appName] = *appThrottle
4044
}
45+
46+
for hostName, expireAt := range f.throttler.SkippedHostsMap() {
47+
snapshot.data.skippedHosts[hostName] = expireAt
48+
}
49+
4150
return snapshot, nil
4251
}
4352

@@ -52,7 +61,12 @@ func (f *fsm) Restore(rc io.ReadCloser) error {
5261
for appName, appThrottle := range data.throttledApps {
5362
f.throttler.ThrottleApp(appName, appThrottle.ExpireAt, appThrottle.Ratio)
5463
}
55-
log.Debugf("freno/raft: restored from snapshot: %d elements restored", len(data.throttledApps))
64+
log.Debugf("freno/raft: restored from snapshot: %d throttled apps", len(data.throttledApps))
65+
66+
for hostName, expireAt := range data.skippedHosts {
67+
f.throttler.SkipHost(hostName, expireAt)
68+
}
69+
log.Debugf("freno/raft: restored from snapshot: %d skipped hosts", len(data.skippedHosts))
5670
return nil
5771
}
5872

@@ -67,3 +81,13 @@ func (f *fsm) applyUnthrottleApp(appName string) interface{} {
6781
f.throttler.UnthrottleApp(appName)
6882
return nil
6983
}
84+
85+
func (f *fsm) applySkipHost(hostName string, expireAt time.Time) interface{} {
86+
f.throttler.SkipHost(hostName, expireAt)
87+
return nil
88+
}
89+
90+
func (f *fsm) applyRecoverHost(hostName string) interface{} {
91+
f.throttler.RecoverHost(hostName)
92+
return nil
93+
}

pkg/group/fsm_snapshot.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package group
22

33
import (
44
"encoding/json"
5+
"time"
56

67
"github.com/github/freno/pkg/base"
78

@@ -12,11 +13,13 @@ import (
1213
// it will mostly duplicate data stored in `throttler`.
1314
type snapshotData struct {
1415
throttledApps map[string](base.AppThrottle)
16+
skippedHosts map[string]time.Time
1517
}
1618

1719
func newSnapshotData() *snapshotData {
1820
return &snapshotData{
1921
throttledApps: make(map[string](base.AppThrottle)),
22+
skippedHosts: make(map[string]time.Time),
2023
}
2124
}
2225

pkg/group/mysql.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ CREATE TABLE throttled_apps (
2828
ratio DOUBLE,
2929
PRIMARY KEY (app_name)
3030
);
31+
32+
CREATE TABLE skipped_hosts (
33+
host_name varchar(128) NOT NULL,
34+
skipped_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
35+
expires_at TIMESTAMP NOT NULL,
36+
PRIMARY KEY (host_name)
37+
);
3138
*/
3239

3340
package group
@@ -145,6 +152,7 @@ func (backend *MySQLBackend) onLeaderStateChange(newLeaderState int64) error {
145152
if newLeaderState > 0 {
146153
log.Infof("Transitioned into leader state")
147154
backend.readThrottledApps()
155+
backend.readSkippedHosts()
148156
} else {
149157
log.Infof("Transitioned out of leader state")
150158
}
@@ -343,6 +351,24 @@ func (backend *MySQLBackend) readThrottledApps() error {
343351
return err
344352
}
345353

354+
func (backend *MySQLBackend) readSkippedHosts() error {
355+
query := `
356+
select
357+
host_name,
358+
timestampdiff(second, now(), expires_at) as ttl_seconds
359+
from
360+
skipped_hosts
361+
`
362+
err := sqlutils.QueryRowsMap(backend.db, query, func(m sqlutils.RowMap) error {
363+
hostName := m.GetString("host_name")
364+
ttlSeconds := m.GetInt64("ttl_seconds")
365+
expireAt := time.Now().Add(time.Duration(ttlSeconds) * time.Second)
366+
go backend.throttler.SkipHost(hostName, expireAt)
367+
return nil
368+
})
369+
return err
370+
}
371+
346372
func (backend *MySQLBackend) ThrottleApp(appName string, ttlMinutes int64, expireAt time.Time, ratio float64) error {
347373
log.Debugf("throttle-app: app=%s, ttlMinutes=%+v, expireAt=%+v, ratio=%+v", appName, ttlMinutes, expireAt, ratio)
348374
var query string
@@ -389,6 +415,36 @@ func (backend *MySQLBackend) UnthrottleApp(appName string) error {
389415
return err
390416
}
391417

418+
func (backend *MySQLBackend) SkipHost(hostName string, ttlMinutes int64, expireAt time.Time) error {
419+
backend.throttler.SkipHost(hostName, expireAt)
420+
query := `
421+
replace into skipped_hosts (
422+
host_name, skipped_at, expires_at
423+
) values (
424+
?, now(), now() + interval ? minute
425+
)`
426+
427+
ttl := ttlMinutes
428+
if ttlMinutes == 0 {
429+
ttl = throttle.DefaultSkipTTLMinutes
430+
}
431+
args := sqlutils.Args(hostName, ttl)
432+
_, err := sqlutils.ExecNoPrepare(backend.db, query, args...)
433+
return err
434+
}
435+
436+
func (backend *MySQLBackend) RecoverHost(hostName string) error {
437+
backend.throttler.RecoverHost(hostName)
438+
query := `update skipped_hosts set expires_at=now() where host_name=?`
439+
args := sqlutils.Args(hostName)
440+
_, err := sqlutils.ExecNoPrepare(backend.db, query, args...)
441+
return err
442+
}
443+
444+
func (backend *MySQLBackend) SkippedHostsMap() map[string]time.Time {
445+
return backend.throttler.SkippedHostsMap()
446+
}
447+
392448
func (backend *MySQLBackend) RecentAppsMap() (result map[string](*base.RecentApp)) {
393449
return backend.throttler.RecentAppsMap()
394450
}

pkg/group/store.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
"github.com/github/freno/pkg/throttle"
2020

2121
"github.com/github/freno/internal/raft"
22-
"github.com/github/freno/internal/raft-boltdb"
22+
raftboltdb "github.com/github/freno/internal/raft-boltdb"
2323
"github.com/outbrain/golib/log"
2424
metrics "github.com/rcrowley/go-metrics"
2525
)
@@ -156,6 +156,27 @@ func (store *Store) UnthrottleApp(appName string) error {
156156
return store.genericCommand(c)
157157
}
158158

159+
func (store *Store) SkipHost(hostName string, ttlMinutes int64, expireAt time.Time) error {
160+
c := &command{
161+
Operation: "skip",
162+
Key: hostName,
163+
ExpireAt: expireAt,
164+
}
165+
return store.genericCommand(c)
166+
}
167+
168+
func (store *Store) RecoverHost(hostName string) error {
169+
c := &command{
170+
Operation: "recover",
171+
Key: hostName,
172+
}
173+
return store.genericCommand(c)
174+
}
175+
176+
func (store *Store) SkippedHostsMap() map[string]time.Time {
177+
return store.throttler.SkippedHostsMap()
178+
}
179+
159180
func (store *Store) ThrottledAppsMap() (result map[string](*base.AppThrottle)) {
160181
return store.throttler.ThrottledAppsMap()
161182
}

pkg/http/api.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ type API interface {
3636
ThrottleApp(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
3737
UnthrottleApp(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
3838
ThrottledApps(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
39+
SkipHost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
40+
SkippedHosts(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
41+
RecoverHost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
3942
RecentApps(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
4043
Help(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
4144
MemcacheConfig(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
@@ -196,7 +199,7 @@ func (api *APIImpl) ReadCheck(w http.ResponseWriter, r *http.Request, ps httprou
196199
api.readCheck(w, r, ps, &throttle.CheckFlags{})
197200
}
198201

199-
// WriteCheckIfExists checks for a metric, but reports an OK if the metric does not exist.
202+
// ReadCheckIfExists checks for a metric, but reports an OK if the metric does not exist.
200203
// If the metric does exist, then all usual checks are made.
201204
func (api *APIImpl) ReadCheckIfExists(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
202205
api.readCheck(w, r, ps, &throttle.CheckFlags{OKIfNotExists: true})
@@ -354,6 +357,39 @@ func metricsHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params)
354357
handler.ServeHTTP(w, r)
355358
}
356359

360+
func (api *APIImpl) SkipHost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
361+
var ttlMinutes int64
362+
var expireAt time.Time
363+
var err error
364+
365+
hostName := ps.ByName("hostName")
366+
ttlString := ps.ByName("ttlMinutes")
367+
if ttlString == "" {
368+
ttlMinutes = 0
369+
} else if ttlMinutes, err = strconv.ParseInt(ttlString, 10, 64); err != nil {
370+
api.respondGeneric(w, r, err)
371+
}
372+
if ttlMinutes != 0 {
373+
expireAt = time.Now().Add(time.Duration(ttlMinutes) * time.Minute)
374+
}
375+
376+
err = api.consensusService.SkipHost(hostName, ttlMinutes, expireAt)
377+
378+
api.respondGeneric(w, r, err)
379+
}
380+
381+
func (api *APIImpl) RecoverHost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
382+
hostName := ps.ByName("hostName")
383+
err := api.consensusService.RecoverHost(hostName)
384+
385+
api.respondGeneric(w, r, err)
386+
}
387+
388+
func (api *APIImpl) SkippedHosts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
389+
w.Header().Set("Content-Type", "application/json")
390+
json.NewEncoder(w).Encode(api.consensusService.SkippedHostsMap())
391+
}
392+
357393
// ConfigureRoutes configures a set of HTTP routes to be actions dispatched by the
358394
// given api's methods.
359395
func ConfigureRoutes(api API) *httprouter.Router {
@@ -387,6 +423,11 @@ func ConfigureRoutes(api API) *httprouter.Router {
387423
register(router, "/recent-apps", api.RecentApps)
388424
register(router, "/recent-apps/:lastMinutes", api.RecentApps)
389425

426+
register(router, "/skip-host/:hostName", api.SkipHost)
427+
register(router, "/skip-host/:hostName/ttl/:ttlMinutes", api.SkipHost)
428+
register(router, "/skipped-hosts", api.SkippedHosts)
429+
register(router, "/recover-host/:hostName", api.RecoverHost)
430+
390431
register(router, "/debug/vars", metricsHandle)
391432
register(router, "/debug/metrics", metricsHandle)
392433

0 commit comments

Comments
 (0)