├── .gitignore ├── LICENSE ├── README.md ├── action.go ├── action_test.go ├── actor.go ├── actor_test.go ├── benchmark_test.go ├── director.go ├── director_test.go ├── go.mod ├── infraction.go ├── infraction_test.go ├── jail.go ├── jail_test.go ├── message.go ├── message_test.go ├── rule.go ├── rule_test.go ├── status.go ├── status_test.go ├── studio.go ├── studio_bench_test.go └── studio_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | .DS_Store 4 | .DS_store 5 | *.pprof 6 | *.out 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jared Folkins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![badactor logo](https://raw.githubusercontent.com/jaredfolkins/badactor_logo/master/badactor_logo_300x300.png) [![Coverage Status](https://img.shields.io/coveralls/jaredfolkins/badactor.svg)](https://coveralls.io/r/jaredfolkins/badactor?branch=master) [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/jaredfolkins/badactor) 2 | 3 | # BadActor 4 | 5 | BadActor is an in-memory, application driven jailer built in the spirit of fail2ban. A middleware with the primary goal to increase the expense for "bad actors" who engage in system probing or attacks. 6 | 7 | The BadActor logo is based on [Renee French's](http://reneefrench.blogspot.com) wonderful gopher. Thanks Renee! 8 | # Install 9 | 10 | ```bash 11 | $ go get github.com/jaredfolkins/badactor 12 | ``` 13 | 14 | # Use Case 15 | 16 | A common use case for BadActor is jailing an offender who fails to login to your website (N) times as this can signal a bruteforce attempt. 17 | 18 | # Tutorial 19 | 20 | Checkout [badactor.org](http://badactor.org) for a tutorial. 21 | 22 | 23 | # Design 24 | 25 | - speed (subsecond response underload and submillisecond with standard operations) 26 | - no external dependencies 27 | - solid code coverage and thorough tests 28 | 29 | # Does It Scale? 30 | 31 | BadActor can be included in your go application and ran concurrently. This allows you an easy way to scale up as BadActor's memory footprint is tiny. Because it leverages a light-weight cache with sharding and reaping, it allows most organizations to be confident that BadActor will not be a bottleneck. 32 | 33 | # Benchmarks 34 | 35 | | Type | Value | 36 | | --- | --- | 37 | | **Model Name** | MacBook Pro | 38 | | **Model Identifier** | MacBookPro11,3 | 39 | | **Processor Name** | Intel Core i7 | 40 | | **Processor Speed** | 2.3 GHz | 41 | | **Number of Processors** | 1 | 42 | | **Total Number of Cores** | 4 | 43 | | **L2 Cache (per Core)** | 256 KB | 44 | | **L3 Cache** | 6 MB | 45 | | **Memory** | 16 GB | 46 | 47 | ###### 1.8.2015 48 | 49 | ```bash 50 | ➜ badactor git:(master) ✗ go test -bench=. -cpu=4 -benchmem -benchtime=5s | column -t 51 | PASS 52 | BenchmarkIsJailed-4 50000000 121 ns/op 0 B/op 0 allocs/op 53 | BenchmarkIsJailedFor-4 50000000 134 ns/op 0 B/op 0 allocs/op 54 | BenchmarkInfraction-4 5000000 1390 ns/op 528 B/op 7 allocs/op 55 | BenchmarkInfractionlIsJailed-4 3000000 2755 ns/op 800 B/op 9 allocs/op 56 | BenchmarkInfractionlIsJailedFor-4 3000000 2733 ns/op 800 B/op 9 allocs/op 57 | BenchmarkStudioInfraction512-4 3000000 2215 ns/op 591 B/op 9 allocs/op 58 | BenchmarkStudioInfraction1024-4 3000000 2357 ns/op 612 B/op 9 allocs/op 59 | BenchmarkStudioInfraction2048-4 5000000 2617 ns/op 621 B/op 9 allocs/op 60 | BenchmarkStudioInfraction4096-4 5000000 2566 ns/op 671 B/op 9 allocs/op 61 | BenchmarkStudioInfraction65536-4 3000000 3309 ns/op 667 B/op 9 allocs/op 62 | BenchmarkStudioInfraction262144-4 2000000 3644 ns/op 674 B/op 9 allocs/op 63 | ok github.com/jaredfolkins/badactor 178.239s 64 | ➜ badactor git:(master) ✗ 65 | ``` 66 | 67 | ###### 12.30.2014 68 | 69 | ```bash 70 | ➜ badactor git:(master) ✗ go test -benchtime=5s -bench=. -benchmem -cpu=4 | column -t 71 | PASS 72 | BenchmarkIsJailed-4 50000000 133 ns/op 0 B/op 0 allocs/op 73 | BenchmarkIsJailedFor-4 50000000 136 ns/op 0 B/op 0 allocs/op 74 | BenchmarkInfraction-4 10000000 824 ns/op 116 B/op 5 allocs/op 75 | BenchmarkInfractionMostCostly-4 10000000 891 ns/op 116 B/op 5 allocs/op 76 | BenchmarkInfractionIsJailed-4 3000000 2569 ns/op 340 B/op 13 allocs/op 77 | BenchmarkInfractionIsJailedFor-4 3000000 2611 ns/op 340 B/op 13 allocs/op 78 | Benchmark10000Actors1Infraction-4 1000 8571335 ns/op 1162931 B/op 50023 allocs/op 79 | Benchmark100000Actors1Infraction-4 100 87687224 ns/op 11630938 B/op 500248 allocs/op 80 | Benchmark1000000Actors1Infraction-4 10 841989544 ns/op 116292788 B/op 5002740 allocs/op 81 | Benchmark10000Actors4Infractions-4 200 30728688 ns/op 4522659 B/op 170013 allocs/op 82 | ok github.com/jaredfolkins/badactor 93.868s 83 | ➜ badactor git:(master) ✗ 84 | 85 | ``` 86 | 87 | ###### 12.24.2014 88 | 89 | ```bash 90 | ➜ badactor git:(master) ✗ go test -bench=. -benchtime=5s -benchmem | column -t 91 | PASS 92 | BenchmarkIsJailed 50000000 138 ns/op 0 B/op 0 allocs/op 93 | BenchmarkIsJailedFor 50000000 140 ns/op 0 B/op 0 allocs/op 94 | BenchmarkInfraction 10000000 943 ns/op 128 B/op 4 allocs/op 95 | BenchmarkInfractionMostCostly 10000000 1008 ns/op 128 B/op 4 allocs/op 96 | Benchmark10000Actors 100 140566388 ns/op 13150354 B/op 150598 allocs/op 97 | Benchmark10000Actors4Infractions 50 241030802 ns/op 17278074 B/op 210614 allocs/op 98 | ok github.com/jaredfolkins/badactor 73.592s 99 | ➜ badactor git:(master) ✗ 100 | 101 | ``` 102 | 103 | ###### 12.16.2014 104 | 105 | This was **before** a serious refactoring. I am keeping it here because **(a)** I'd like to encourage others to *benchmark* their code and **(b)** I learned many valuable lessons while doing it. 106 | 107 | ```bash 108 | ➜ badactor git:(master) go test -bench=. -benchtime=5s -benchmem | column -t 109 | PASS 110 | BenchmarkInfraction1 2000 2679694 ns/op 518 B/op 10 allocs/op 111 | BenchmarkInfraction10 2000 3050845 ns/op 516 B/op 10 allocs/op 112 | BenchmarkInfraction100 2000 3430051 ns/op 516 B/op 10 allocs/op 113 | BenchmarkInfraction1000 2000 3738125 ns/op 516 B/op 10 allocs/op 114 | BenchmarkInfraction10000 2000 4004534 ns/op 516 B/op 10 allocs/op 115 | BenchmarkInfractionWithIsJailed1 3000 1832770 ns/op 193 B/op 3 allocs/op 116 | BenchmarkInfractionWithIsJailed10 3000 1968030 ns/op 193 B/op 3 allocs/op 117 | BenchmarkInfractionWithIsJailed100 3000 2120179 ns/op 193 B/op 3 allocs/op 118 | BenchmarkInfractionWithIsJailed1000 3000 1955656 ns/op 193 B/op 3 allocs/op 119 | BenchmarkInfractionWithIsJailed10000 3000 1943728 ns/op 193 B/op 3 allocs/op 120 | ok github.com/jaredfolkins/badactor 109.879s 121 | ➜ badactor git:(master) 122 | ``` 123 | 124 | # Action Interface 125 | 126 | The Action Interface has two primary methods, **WhenJailed** and **WhenTimeServed**. An excerpt of an implementation is below. They are called when the actor is jailed for the rule or when the actor has served its time for a particular rule. 127 | 128 | 129 | ```go 130 | type MyAction struct{} 131 | 132 | func (ma *MyAction) WhenJailed(a *badactor.Actor, r *badactor.Rule) error { 133 | // Do something here. Log, email, etc... 134 | return nil 135 | } 136 | 137 | func (ma *MyAction) WhenTimeServed(a *badactor.Actor, r *badactor.Rule) error { 138 | // Do something here. Log, email, etc... 139 | return nil 140 | } 141 | ``` 142 | 143 | And assigned to the rule like so. 144 | 145 | ```go 146 | // define and add the rule to the stack 147 | ru := &badactor.Rule{ 148 | Name: "Login", 149 | Message: "You have failed to login too many times", 150 | StrikeLimit: 10, 151 | ExpireBase: time.Second * 1, 152 | Sentence: time.Second * 10, 153 | Action: &MyAction{}, 154 | } 155 | st.AddRule(ru) 156 | ``` 157 | 158 | # Httprouter & Negroni Example 159 | 160 | ```go 161 | package main 162 | 163 | import ( 164 | "log" 165 | "net" 166 | "net/http" 167 | "time" 168 | 169 | "github.com/urfave/negroni" 170 | "github.com/jaredfolkins/badactor" 171 | "github.com/julienschmidt/httprouter" 172 | ) 173 | 174 | var st *badactor.Studio 175 | 176 | type MyAction struct{} 177 | 178 | func (ma *MyAction) WhenJailed(a *badactor.Actor, r *badactor.Rule) error { 179 | return nil 180 | } 181 | 182 | func (ma *MyAction) WhenTimeServed(a *badactor.Actor, r *badactor.Rule) error { 183 | return nil 184 | } 185 | 186 | func main() { 187 | 188 | //runtime.GOMAXPROCS(4) 189 | 190 | // studio capacity 191 | var sc int32 192 | // director capacity 193 | var dc int32 194 | 195 | sc = 1024 196 | dc = 1024 197 | 198 | // init new Studio 199 | st = badactor.NewStudio(sc) 200 | 201 | // define and add the rule to the stack 202 | ru := &badactor.Rule{ 203 | Name: "Login", 204 | Message: "You have failed to login too many times", 205 | StrikeLimit: 10, 206 | ExpireBase: time.Second * 1, 207 | Sentence: time.Second * 10, 208 | Action: &MyAction{}, 209 | } 210 | st.AddRule(ru) 211 | 212 | err := st.CreateDirectors(dc) 213 | if err != nil { 214 | log.Fatal(err) 215 | } 216 | 217 | //poll duration 218 | dur := time.Minute * time.Duration(60) 219 | // Start the reaper 220 | st.StartReaper(dur) 221 | 222 | // router 223 | router := httprouter.New() 224 | router.POST("/login", LoginHandler) 225 | 226 | // middleware 227 | n := negroni.Classic() 228 | n.Use(NewBadActorMiddleware()) 229 | n.UseHandler(router) 230 | n.Run(":9999") 231 | 232 | } 233 | 234 | // 235 | // HANDLER 236 | // 237 | 238 | // this is a niave login function for example purposes 239 | func LoginHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 240 | 241 | var err error 242 | 243 | un := r.FormValue("username") 244 | pw := r.FormValue("password") 245 | 246 | // snag the IP for use as the actor's name 247 | an, _, err := net.SplitHostPort(r.RemoteAddr) 248 | if err != nil { 249 | panic(err) 250 | } 251 | 252 | // mock authentication 253 | if un == "example_user" && pw == "example_pass" { 254 | http.Redirect(w, r, "", http.StatusOK) 255 | return 256 | } 257 | 258 | // auth fails, increment infraction 259 | err = st.Infraction(an, "Login") 260 | if err != nil { 261 | log.Printf("[%v] has err %v", an, err) 262 | } 263 | 264 | // auth fails, increment infraction 265 | i, err := st.Strikes(an, "Login") 266 | log.Printf("[%v] has %v Strikes %v", an, i, err) 267 | 268 | http.Redirect(w, r, "", http.StatusUnauthorized) 269 | return 270 | } 271 | 272 | // 273 | // MIDDLEWARE 274 | // 275 | type BadActorMiddleware struct { 276 | negroni.Handler 277 | } 278 | 279 | func NewBadActorMiddleware() *BadActorMiddleware { 280 | return &BadActorMiddleware{} 281 | } 282 | 283 | func (bam *BadActorMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 284 | 285 | // snag the IP for use as the actor's name 286 | an, _, err := net.SplitHostPort(r.RemoteAddr) 287 | if err != nil { 288 | panic(err) 289 | } 290 | 291 | // if the Actor is jailed, send them StatusUnauthorized 292 | if st.IsJailed(an) { 293 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 294 | return 295 | } 296 | 297 | // call the next middleware in the chain 298 | next(w, r) 299 | } 300 | ``` 301 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /action.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | // Action is the inferface the Programmer implements to perform event based actions 4 | type Action interface { 5 | WhenJailed(a *Actor, r *Rule) error // When an Actor isJailed, do this 6 | WhenTimeServed(a *Actor, r *Rule) error // When an Actor is relased because of timeServed, do this 7 | } 8 | -------------------------------------------------------------------------------- /action_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var whenJailedCounter = 0 10 | var whenTimeServedCounter = 0 11 | 12 | type MockAction struct{} 13 | 14 | func (ma *MockAction) Log(a *Actor, r *Rule) {} 15 | func (ma *MockAction) WhenJailed(a *Actor, r *Rule) error { 16 | whenJailedCounter++ 17 | ma.Log(a, r) 18 | return nil 19 | } 20 | func (ma *MockAction) WhenTimeServed(a *Actor, r *Rule) error { 21 | whenTimeServedCounter++ 22 | ma.Log(a, r) 23 | return nil 24 | } 25 | 26 | func TestActionWhenJailed(t *testing.T) { 27 | //setup 28 | var b bool 29 | d := NewDirector(ia) 30 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 31 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 32 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 33 | sl := 3 34 | r := &Rule{ 35 | Name: rn, 36 | Message: rm, 37 | StrikeLimit: sl, 38 | ExpireBase: time.Second * 60, 39 | Sentence: time.Second * 60, 40 | Action: &MockAction{}, 41 | } 42 | a := newClassicActor(an, r, d) 43 | 44 | //test 45 | for i := 0; i < 3; i++ { 46 | a.infraction(rn) 47 | } 48 | 49 | b = a.isJailedFor(an) 50 | if b == true { 51 | t.Errorf("isJailedFor should be false instead [%v]", b) 52 | } 53 | 54 | if whenJailedCounter != 1 { 55 | t.Errorf("whenJailedCounter should be 1 instead [%v]", whenJailedCounter) 56 | } 57 | 58 | } 59 | 60 | func TestActionWhenTimeServed(t *testing.T) { 61 | //setup 62 | var b bool 63 | 64 | d := NewDirector(ia) 65 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 66 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 67 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 68 | sl := 3 69 | r := &Rule{ 70 | Name: rn, 71 | Message: rm, 72 | StrikeLimit: sl, 73 | ExpireBase: time.Second * 1, 74 | Sentence: time.Millisecond * 10, 75 | Action: &MockAction{}, 76 | } 77 | a := newClassicActor(an, r, d) 78 | 79 | if whenTimeServedCounter != 0 { 80 | t.Errorf("whenTimeServedCounter want [0] has [%v]", whenTimeServedCounter) 81 | } 82 | //test 83 | for i := 0; i < 3; i++ { 84 | err := a.infraction(rn) 85 | if err != nil { 86 | t.Errorf("infraction %v should not error", err) 87 | } 88 | } 89 | 90 | if whenTimeServedCounter != 0 { 91 | t.Errorf("whenTimeServedCounter want [0] has [%v]", whenTimeServedCounter) 92 | } 93 | 94 | b = a.isJailed() 95 | if b == false { 96 | t.Errorf("isJailed should be true instead [%v]", b) 97 | } 98 | 99 | b = a.isJailedFor(rn) 100 | if b == false { 101 | t.Errorf("isJailedFor should be true instead [%v]", b) 102 | } 103 | 104 | if whenTimeServedCounter != 0 { 105 | t.Errorf("whenTimeServedCounter want [0] has [%v]", whenTimeServedCounter) 106 | } 107 | 108 | dur := time.Now().Add(-time.Hour * 1) 109 | a.jails[rn].releaseBy = dur 110 | 111 | for _, j := range a.jails { 112 | a.timeServed(j) 113 | } 114 | 115 | if whenTimeServedCounter != 1 { 116 | t.Errorf("whenTimeServedCounter want [1] has [%v]", whenTimeServedCounter) 117 | } 118 | 119 | } 120 | 121 | var MANAwhenJailedCounter = 0 122 | var MANAwhenTimeServedCounter = 0 123 | 124 | type MockActorNameAction struct{} 125 | 126 | func (ma *MockActorNameAction) Log(a *Actor, r *Rule) {} 127 | func (ma *MockActorNameAction) WhenJailed(a *Actor, r *Rule) error { 128 | MANAwhenJailedCounter++ 129 | ma.Log(a, r) 130 | // this shouldn't deadlock 131 | a.Name() 132 | // this shouldn't deadlock 133 | a.director.ActorsName(a) 134 | return nil 135 | } 136 | func (ma *MockActorNameAction) WhenTimeServed(a *Actor, r *Rule) error { 137 | MANAwhenTimeServedCounter++ 138 | ma.Log(a, r) 139 | // this shouldn't deadlock 140 | a.Name() 141 | // this shouldn't deadlock 142 | a.director.ActorsName(a) 143 | return nil 144 | } 145 | 146 | func TestActorNameAction(t *testing.T) { 147 | //setup 148 | var b bool 149 | 150 | d := NewDirector(ia) 151 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 152 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 153 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 154 | sl := 3 155 | r := &Rule{ 156 | Name: rn, 157 | Message: rm, 158 | StrikeLimit: sl, 159 | ExpireBase: time.Second * 1, 160 | Sentence: time.Millisecond * 10, 161 | Action: &MockActorNameAction{}, 162 | } 163 | a := newClassicActor(an, r, d) 164 | 165 | if MANAwhenTimeServedCounter != 0 { 166 | t.Errorf("MANAwhenTimeServedCounter want [0] has [%v]", MANAwhenTimeServedCounter) 167 | } 168 | //test 169 | for i := 0; i < 3; i++ { 170 | err := a.infraction(rn) 171 | if err != nil { 172 | t.Errorf("infraction %v should not error", err) 173 | } 174 | } 175 | 176 | if MANAwhenTimeServedCounter != 0 { 177 | t.Errorf("whenTimeServedCounter want [0] has [%v]", MANAwhenTimeServedCounter) 178 | } 179 | 180 | b = a.isJailed() 181 | if b == false { 182 | t.Errorf("isJailed should be true instead [%v]", b) 183 | } 184 | 185 | b = a.isJailedFor(rn) 186 | if b == false { 187 | t.Errorf("isJailedFor should be true instead [%v]", b) 188 | } 189 | 190 | if MANAwhenTimeServedCounter != 0 { 191 | t.Errorf("whenTimeServedCounter want [0] has [%v]", MANAwhenTimeServedCounter) 192 | } 193 | 194 | dur := time.Now().Add(-time.Hour * 1) 195 | a.jails[rn].releaseBy = dur 196 | 197 | for _, j := range a.jails { 198 | a.timeServed(j) 199 | } 200 | 201 | if MANAwhenTimeServedCounter != 1 { 202 | t.Errorf("whenTimeServedCounter want [1] has [%v]", MANAwhenTimeServedCounter) 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /actor.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ttl is the time to live value for newly created actors 9 | const ttl = 100 10 | 11 | type Actor struct { 12 | name string 13 | infractions map[string]*infraction 14 | jails map[string]*jail 15 | director *Director 16 | ttl time.Time 17 | accessedAt time.Time 18 | } 19 | 20 | func newActor(n string, d *Director) *Actor { 21 | a := &Actor{ 22 | director: d, 23 | ttl: time.Now().Add(time.Millisecond * ttl), 24 | name: n, 25 | infractions: make(map[string]*infraction), 26 | jails: make(map[string]*jail), 27 | accessedAt: time.Now(), 28 | } 29 | return a 30 | } 31 | 32 | func (a *Actor) Name() string { 33 | return a.name 34 | } 35 | 36 | func newClassicActor(n string, r *Rule, d *Director) *Actor { 37 | a := &Actor{ 38 | director: d, 39 | ttl: time.Now().Add(time.Millisecond * ttl), 40 | name: n, 41 | infractions: make(map[string]*infraction), 42 | jails: make(map[string]*jail), 43 | accessedAt: time.Now(), 44 | } 45 | 46 | a.infractions[r.Name] = newInfraction(r) 47 | return a 48 | } 49 | 50 | func (a *Actor) rebaseAll() error { 51 | 52 | /* I don't believe this is needed, tests pass, will think a bit more before removing 53 | for _, inf := range a.infractions { 54 | inf.rebase() 55 | } 56 | */ 57 | 58 | a.ttl = time.Now().Add(time.Millisecond * ttl) 59 | 60 | return nil 61 | } 62 | 63 | func (a *Actor) infraction(rn string) error { 64 | 65 | if a.isJailedFor(rn) { 66 | return fmt.Errorf("actor [%v] is already Jailed for [%v]", a.name, rn) 67 | } 68 | 69 | if _, ok := a.infractions[rn]; ok { 70 | inf := a.infractions[rn] 71 | inf.strikes++ 72 | inf.rebase() 73 | return a.jail(rn) 74 | } 75 | 76 | return fmt.Errorf("Infraction against actor [%v]", a.name) 77 | } 78 | 79 | func (a *Actor) strikes(rn string) int { 80 | if _, ok := a.infractions[rn]; ok { 81 | return a.infractions[rn].strikes 82 | } 83 | return 0 84 | } 85 | 86 | func (a *Actor) isJailedFor(rn string) bool { 87 | _, ok := a.jails[rn] 88 | return ok 89 | } 90 | 91 | func (a *Actor) isJailed() bool { 92 | if len(a.jails) > 0 { 93 | return true 94 | } 95 | return false 96 | } 97 | 98 | // shouldDelete returns a bool if the Infractions and Jails maps are empty and the ttl is expired 99 | func (a *Actor) shouldDelete() bool { 100 | if !a.hasInfractions() && !a.hasJails() { 101 | if time.Now().After(a.ttl) { 102 | return true 103 | } 104 | } 105 | return false 106 | } 107 | 108 | func (a *Actor) timeServed(j *jail) bool { 109 | if time.Now().After(j.releaseBy) && j != nil { 110 | if j.rule.Action != nil { 111 | ca := a // copy actor 112 | cr := j.rule // copy rule 113 | j.rule.Action.WhenTimeServed(ca, cr) 114 | } 115 | delete(a.jails, j.rule.Name) 116 | a.rebaseAll() 117 | return true 118 | } 119 | return false 120 | } 121 | 122 | func (a *Actor) expire(rn string) error { 123 | 124 | // validate key exists 125 | if _, ok := a.infractions[rn]; !ok { 126 | return fmt.Errorf("Infraction [%v] does not exist", rn) 127 | } 128 | 129 | if time.Now().After(a.infractions[rn].expireBy) { 130 | delete(a.infractions, rn) 131 | return nil 132 | } 133 | 134 | return fmt.Errorf("Could not expire [%v]", rn) 135 | } 136 | 137 | // jail the actor if the Limit has been reached 138 | func (a *Actor) jail(rn string) error { 139 | 140 | if !a.infractionExists(rn) { 141 | return fmt.Errorf("jail failed, infraction [%v] does not exist", rn) 142 | } 143 | 144 | inf := a.infractions[rn] 145 | 146 | if inf.strikes >= inf.rule.StrikeLimit { 147 | j := newJail(inf.rule, inf.rule.Sentence) 148 | a.jails[inf.rule.Name] = j 149 | delete(a.infractions, inf.rule.Name) 150 | if inf.rule.Action != nil { 151 | ca := a // copy actor 152 | cr := inf.rule // copy rule 153 | inf.rule.Action.WhenJailed(ca, cr) 154 | } 155 | a.rebaseAll() 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func (a *Actor) createInfraction(inf *infraction) error { 162 | if _, exists := a.infractions[inf.rule.Name]; !exists { 163 | a.infractions[inf.rule.Name] = inf 164 | return nil 165 | } 166 | return fmt.Errorf("Unable to create infraction [%v]", inf.rule.Name) 167 | } 168 | 169 | func (a *Actor) hasInfractions() bool { 170 | if len(a.infractions) > 0 { 171 | return true 172 | } 173 | return false 174 | } 175 | 176 | func (a *Actor) infractionExists(rn string) bool { 177 | _, ok := a.infractions[rn] 178 | return ok 179 | } 180 | 181 | func (a *Actor) hasJails() bool { 182 | if len(a.jails) > 0 { 183 | return true 184 | } 185 | return false 186 | } 187 | 188 | func (a *Actor) totalJails() int { 189 | return len(a.jails) 190 | } 191 | 192 | func (a *Actor) timeToLive() time.Time { 193 | return a.ttl 194 | } 195 | -------------------------------------------------------------------------------- /actor_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const ia = 1024 10 | 11 | type ActorMockAction struct{} 12 | 13 | func (ama *ActorMockAction) WhenJailed(a *Actor, r *Rule) error { 14 | return nil 15 | } 16 | func (ama *ActorMockAction) WhenTimeServed(a *Actor, r *Rule) error { 17 | return nil 18 | } 19 | 20 | func TestActorIsJailedFor(t *testing.T) { 21 | //setup 22 | var b bool 23 | d := NewDirector(ia) 24 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 25 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 26 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 27 | sl := 3 28 | r := &Rule{ 29 | Name: rn, 30 | Message: rm, 31 | StrikeLimit: sl, 32 | ExpireBase: time.Second * 60, 33 | Sentence: time.Second * 60, 34 | Action: &ActorMockAction{}, 35 | } 36 | a := newClassicActor(an, r, d) 37 | 38 | //test 39 | for i := 0; i < 3; i++ { 40 | a.infraction(rn) 41 | } 42 | 43 | b = a.isJailedFor(an) 44 | if b { 45 | t.Errorf("isJailedFor should be false instead [%v]", b) 46 | } 47 | 48 | for i := 0; i < 3; i++ { 49 | a.infraction(rn) 50 | } 51 | 52 | b = a.isJailedFor(rn) 53 | if !b { 54 | t.Errorf("isJailedFor should be false instead [%v]", b) 55 | } 56 | 57 | } 58 | 59 | func TestActorJail(t *testing.T) { 60 | // setup 61 | var b bool 62 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 63 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 64 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 65 | d := NewDirector(ia) 66 | sl := 3 67 | r := &Rule{ 68 | Name: rn, 69 | Message: rm, 70 | StrikeLimit: sl, 71 | ExpireBase: time.Second * 60, 72 | Sentence: time.Millisecond * 3, 73 | Action: &ActorMockAction{}, 74 | } 75 | a := newClassicActor(an, r, d) 76 | 77 | // test 78 | for i := 0; i < 3; i++ { 79 | a.infraction(rn) 80 | } 81 | 82 | b = a.isJailedFor(rn) 83 | if b == false { 84 | t.Errorf("isJailedFor should be true instead [%v]", b) 85 | } 86 | 87 | } 88 | 89 | func TestActorTimeServed(t *testing.T) { 90 | // setup 91 | var err error 92 | var b bool 93 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 94 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 95 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 96 | dur := time.Millisecond * 10 97 | d := NewDirector(ia) 98 | sl := 3 99 | r := &Rule{ 100 | Name: rn, 101 | Message: rm, 102 | StrikeLimit: sl, 103 | ExpireBase: time.Second * 60, 104 | Sentence: time.Millisecond * 50, 105 | Action: &ActorMockAction{}, 106 | } 107 | a := newClassicActor(an, r, d) 108 | 109 | b = a.isJailedFor(rn) 110 | if b == true { 111 | t.Errorf("isJailedFor should be false instead [%v]", b) 112 | } 113 | 114 | // test 115 | for i := 0; i < 3; i++ { 116 | err = a.infraction(rn) 117 | if err != nil { 118 | t.Errorf("Infraction should not error instead [%v]", err) 119 | } 120 | } 121 | 122 | // .lockup should happen in the goroutine background 123 | time.Sleep(dur) 124 | b = a.isJailedFor(rn) 125 | if b == false { 126 | t.Errorf("isJailedFor should be true instead [%v]", b) 127 | } 128 | 129 | time.Sleep(time.Millisecond * 40) 130 | for _, s := range a.jails { 131 | a.timeServed(s) 132 | } 133 | b = a.isJailedFor(rn) 134 | if b == true { 135 | t.Errorf("isJailedFor should be false instead [%v]", b) 136 | } 137 | 138 | } 139 | 140 | func TestActorExpire(t *testing.T) { 141 | 142 | var err error 143 | br := "badrule" 144 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 145 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 146 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 147 | dur := time.Millisecond * 40 148 | d := NewDirector(ia) 149 | r := &Rule{ 150 | Name: rn, 151 | Message: rm, 152 | StrikeLimit: 3, 153 | ExpireBase: time.Millisecond * 50, 154 | Sentence: time.Millisecond * 50, 155 | Action: &ActorMockAction{}, 156 | } 157 | 158 | a := newClassicActor(an, r, d) 159 | 160 | err = a.expire(br) 161 | if err == nil { 162 | t.Errorf("Expire should error for [%v]", err) 163 | } 164 | 165 | a.infraction(rn) 166 | 167 | err = a.expire(br) 168 | if err == nil { 169 | t.Errorf("Expire should error for [%v]", err) 170 | } 171 | 172 | time.Sleep(dur) 173 | time.Sleep(dur) 174 | 175 | err = a.expire(rn) 176 | if err != nil { 177 | t.Errorf("Expire should delete [%v:%v]", an, rn) 178 | } 179 | 180 | if a.hasInfractions() { 181 | t.Errorf("Infractions should not exist [%v:%v]", an, rn) 182 | } 183 | 184 | } 185 | 186 | func TestActorInfraction(t *testing.T) { 187 | 188 | d := NewDirector(ia) 189 | var err error 190 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 191 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 192 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 193 | sl := 3 194 | r := &Rule{ 195 | Name: rn, 196 | Message: rm, 197 | StrikeLimit: sl, 198 | ExpireBase: time.Second * 60, 199 | Sentence: time.Millisecond * 10, 200 | Action: &ActorMockAction{}, 201 | } 202 | 203 | a := newClassicActor(an, r, d) 204 | 205 | rde := "ruledoesntexist" 206 | err = a.infraction(rde) 207 | if err == nil { 208 | t.Errorf("Infraction should error [%v:%v:%v]", err, an, rde) 209 | } 210 | 211 | for i := 0; i < 3; i++ { 212 | err = a.infraction(rn) 213 | if err != nil { 214 | t.Errorf("Infraction should not error [%v:%v]", an, rn) 215 | } 216 | } 217 | 218 | err = a.jail(rn) 219 | if err == nil { 220 | t.Errorf("Jail() err should not be nil instead [%v]", err) 221 | } 222 | 223 | err = a.infraction(rn) 224 | if err == nil { 225 | t.Errorf("Infraction should error [%v:%v]", an, rn) 226 | } 227 | 228 | } 229 | 230 | func TestActorCreateInfraction(t *testing.T) { 231 | 232 | d := NewDirector(ia) 233 | var err error 234 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 235 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 236 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 237 | r := &Rule{ 238 | Name: rn, 239 | Message: rm, 240 | StrikeLimit: 3, 241 | ExpireBase: time.Millisecond * 10, 242 | Sentence: time.Millisecond * 10, 243 | Action: &ActorMockAction{}, 244 | } 245 | 246 | a := newClassicActor(an, r, d) 247 | 248 | inf := newInfraction(r) 249 | 250 | err = a.createInfraction(inf) 251 | if err == nil { 252 | t.Errorf("CreateInfraction [%v:%v] should error %v", an, rn, err) 253 | } 254 | 255 | ninf := a.infractions[rn] 256 | if ninf.rule.Name != rn { 257 | t.Errorf("CreateInfraction Name incorrect [%v:%v]", an, rn) 258 | } 259 | 260 | } 261 | 262 | func TestActorRebaseAll(t *testing.T) { 263 | 264 | d := NewDirector(ia) 265 | var err error 266 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 267 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 268 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 269 | r := &Rule{ 270 | Name: rn, 271 | Message: rm, 272 | StrikeLimit: 3, 273 | ExpireBase: time.Millisecond * 10, 274 | Sentence: time.Millisecond * 10, 275 | Action: &ActorMockAction{}, 276 | } 277 | a := newClassicActor(an, r, d) 278 | 279 | inf := newInfraction(r) 280 | 281 | err = a.createInfraction(inf) 282 | if err == nil { 283 | t.Errorf("CreateInfraction [%v:%v] should error %v", an, rn, err) 284 | } 285 | 286 | ts := time.Now() 287 | a.rebaseAll() 288 | for _, inf := range a.infractions { 289 | if inf.expireBy.Before(ts) { 290 | t.Errorf("RebaseAll failed ExpireBy time is before TimeStamp [%v:%v:%v:%v]", an, rn, inf.expireBy, ts) 291 | } 292 | } 293 | 294 | } 295 | 296 | func TestActorStrikes(t *testing.T) { 297 | var err error 298 | var i int 299 | bn := "badname" 300 | d := NewDirector(ia) 301 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 302 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 303 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 304 | r := &Rule{ 305 | Name: rn, 306 | Message: rm, 307 | StrikeLimit: 3, 308 | ExpireBase: time.Millisecond * 10, 309 | Sentence: time.Millisecond * 10, 310 | Action: &ActorMockAction{}, 311 | } 312 | a := newClassicActor(an, r, d) 313 | 314 | i = a.strikes(rn) 315 | if i != 0 { 316 | t.Errorf("Strike count for [%v:%v] must be 1", an, rn) 317 | } 318 | 319 | err = a.infraction(rn) 320 | if err != nil { 321 | t.Errorf("Infraction should not error") 322 | } 323 | 324 | i = a.strikes(rn) 325 | if i != 1 { 326 | t.Errorf("Strike count for [%v:%v] must be 1", an, rn) 327 | } 328 | 329 | i = a.strikes(bn) 330 | if i != 0 { 331 | t.Errorf("Badname should count for [%v] not be 0", bn) 332 | } 333 | 334 | } 335 | 336 | func TestActorShouldDelete(t *testing.T) { 337 | // setup 338 | var err error 339 | var b bool 340 | d := NewDirector(ia) 341 | dur := time.Millisecond * 100 342 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 343 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 344 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 345 | r := &Rule{ 346 | Name: rn, 347 | Message: rm, 348 | StrikeLimit: 3, 349 | ExpireBase: time.Millisecond * 1, 350 | Sentence: time.Millisecond * 2, 351 | Action: &ActorMockAction{}, 352 | } 353 | a := newClassicActor(an, r, d) 354 | 355 | // test 356 | // assert all falsey 357 | b = a.shouldDelete() 358 | if b == true { 359 | t.Errorf("shouldDelete should be false instead [%v]", b) 360 | } 361 | 362 | b = a.isJailedFor(rn) 363 | if b == true { 364 | t.Errorf("isJailedFor should be false instead [%v]", b) 365 | } 366 | 367 | b = a.hasJails() 368 | if b == true { 369 | t.Errorf("hasJails should be false instead [%v]", b) 370 | } 371 | 372 | // assert all truthy 373 | for i := 0; i < 3; i++ { 374 | err = a.infraction(rn) 375 | if err != nil { 376 | t.Errorf("Infraction [%v] should not error %v", rn, err) 377 | } 378 | } 379 | 380 | b = a.isJailedFor(rn) 381 | if b == false { 382 | t.Errorf("isJailedFor should be true instead [%v]", b) 383 | } 384 | 385 | b = a.hasJails() 386 | if b == false { 387 | t.Errorf("hasJails should be true instead [%v]", b) 388 | } 389 | 390 | // sleep, quit, should NOT be jailed 391 | time.Sleep(dur) 392 | 393 | for _, s := range a.jails { 394 | a.timeServed(s) 395 | } 396 | 397 | b = a.isJailedFor(rn) 398 | if b == true { 399 | t.Errorf("isJailedFor should be false instead [%v]", b) 400 | } 401 | 402 | b = a.hasJails() 403 | if b == true { 404 | t.Errorf("hasJails should be false instead [%v]", b) 405 | } 406 | 407 | b = a.hasInfractions() 408 | if b == true { 409 | t.Errorf("hasInfractions should be false instead [%v]", b) 410 | } 411 | 412 | time.Sleep(dur) 413 | 414 | b = a.shouldDelete() 415 | if b == false { 416 | t.Errorf("shouldDelete should be true instead [%v]", b) 417 | } 418 | 419 | } 420 | 421 | func TestActorInfractionExists(t *testing.T) { 422 | // setup 423 | var b bool 424 | var err error 425 | d := NewDirector(ia) 426 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 427 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 428 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 429 | r := &Rule{ 430 | Name: rn, 431 | Message: rm, 432 | StrikeLimit: 3, 433 | ExpireBase: time.Second * 1, 434 | Sentence: time.Millisecond * 50, 435 | Action: &ActorMockAction{}, 436 | } 437 | a := newActor(an, d) 438 | 439 | b = a.infractionExists(rn) 440 | if b == true { 441 | t.Errorf("infractionExists should be false instead [%v]", b) 442 | } 443 | 444 | inf := newInfraction(r) 445 | err = a.createInfraction(inf) 446 | if err != nil { 447 | t.Errorf("createInfraction should not error %v", err) 448 | } 449 | 450 | b = a.infractionExists(rn) 451 | if b == false { 452 | t.Errorf("infractionExists should be true instead [%v]", b) 453 | } 454 | } 455 | 456 | func TestActorTotalJails(t *testing.T) { 457 | // setup 458 | d := NewDirector(ia) 459 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 460 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 461 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 462 | r := &Rule{ 463 | Name: rn, 464 | Message: rm, 465 | StrikeLimit: 3, 466 | ExpireBase: time.Second * 1, 467 | Sentence: time.Millisecond * 50, 468 | Action: &ActorMockAction{}, 469 | } 470 | a := newClassicActor(an, r, d) 471 | 472 | for i := 0; i < 3; i++ { 473 | a.infraction(rn) 474 | } 475 | 476 | i := a.totalJails() 477 | if i != 1 { 478 | t.Errorf("totalJails should be 1 instead [%v]", i) 479 | 480 | } 481 | 482 | } 483 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func BenchmarkIsJailed(b *testing.B) { 11 | var err error 12 | d := NewDirector(ia) 13 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 14 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 15 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 16 | r := &Rule{ 17 | Name: rn, 18 | Message: rm, 19 | StrikeLimit: 3, 20 | ExpireBase: time.Second * 60, 21 | Sentence: time.Minute * 5, 22 | } 23 | 24 | err = d.lAddRule(r) 25 | if err != nil { 26 | b.Errorf("lAddRule [%s] should not fail", rn) 27 | } 28 | 29 | for i := 0; i < 4; i++ { 30 | d.lInfraction(an, rn) 31 | } 32 | 33 | for i := 0; i < b.N; i++ { 34 | d.lIsJailed(an) 35 | } 36 | } 37 | 38 | func BenchmarkIsJailedFor(b *testing.B) { 39 | var err error 40 | d := NewDirector(ia) 41 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 42 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 43 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 44 | r := &Rule{ 45 | Name: rn, 46 | Message: rm, 47 | StrikeLimit: 3, 48 | ExpireBase: time.Second * 60, 49 | Sentence: time.Minute * 5, 50 | } 51 | 52 | err = d.lAddRule(r) 53 | if err != nil { 54 | b.Errorf("lAddRule [%s] should not fail", rn) 55 | } 56 | 57 | for i := 0; i < 4; i++ { 58 | d.lInfraction(an, rn) 59 | } 60 | 61 | for i := 0; i < b.N; i++ { 62 | d.lIsJailedFor(an, rn) 63 | } 64 | } 65 | 66 | func BenchmarkInfraction(b *testing.B) { 67 | var err error 68 | d := NewDirector(ia) 69 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 70 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 71 | r := &Rule{ 72 | Name: rn, 73 | Message: rm, 74 | StrikeLimit: 3, 75 | ExpireBase: time.Second * 60, 76 | Sentence: time.Minute * 5, 77 | } 78 | 79 | err = d.lAddRule(r) 80 | if err != nil { 81 | b.Errorf("lAddRule for [%v] should not fail", rn) 82 | } 83 | 84 | for i := 0; i < b.N; i++ { 85 | an := strconv.FormatInt(rand.Int63(), 10) 86 | d.lInfraction(an, rn) 87 | } 88 | } 89 | 90 | func BenchmarkInfractionlIsJailed(b *testing.B) { 91 | var err error 92 | d := NewDirector(ia) 93 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 94 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 95 | r := &Rule{ 96 | Name: rn, 97 | Message: rm, 98 | StrikeLimit: 3, 99 | ExpireBase: time.Second * 60, 100 | Sentence: time.Minute * 5, 101 | } 102 | 103 | err = d.lAddRule(r) 104 | if err != nil { 105 | b.Errorf("lAddRule [%s] should not fail", rn) 106 | } 107 | 108 | for i := 0; i < b.N; i++ { 109 | an := strconv.FormatInt(rand.Int63(), 10) 110 | for i := 0; i < 3; i++ { 111 | d.lInfraction(an, rn) 112 | } 113 | d.lIsJailed(an) 114 | } 115 | } 116 | 117 | func BenchmarkInfractionlIsJailedFor(b *testing.B) { 118 | var err error 119 | d := NewDirector(ia) 120 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 121 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 122 | r := &Rule{ 123 | Name: rn, 124 | Message: rm, 125 | StrikeLimit: 3, 126 | ExpireBase: time.Second * 60, 127 | Sentence: time.Minute * 5, 128 | } 129 | 130 | err = d.lAddRule(r) 131 | if err != nil { 132 | b.Errorf("lAddRule [%s] should not fail", rn) 133 | } 134 | 135 | for i := 0; i < b.N; i++ { 136 | an := strconv.FormatInt(rand.Int63(), 10) 137 | for i := 0; i < 3; i++ { 138 | d.lInfraction(an, rn) 139 | } 140 | d.lIsJailedFor(an, rn) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /director.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "container/list" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Director is a bucket/shard and contains many Actors 11 | type Director struct { 12 | sync.Mutex 13 | 14 | index *list.List 15 | actors map[string]*list.Element 16 | rules map[string]*Rule 17 | 18 | capacity int32 19 | size int32 20 | } 21 | 22 | // NewDirector instantiates a Director Struct 23 | func NewDirector(ma int32) *Director { 24 | d := &Director{ 25 | capacity: ma, 26 | index: list.New(), 27 | actors: make(map[string]*list.Element), 28 | rules: make(map[string]*Rule), 29 | } 30 | return d 31 | } 32 | 33 | func (d *Director) lMaintenance() { 34 | d.Lock() 35 | defer d.Unlock() 36 | for e := d.index.Front(); e != nil; e = e.Next() { 37 | an := e.Value.(*Actor).name 38 | d.maintenance(an) 39 | } 40 | } 41 | 42 | // ActorsName accepts an Actor as an argument 43 | // it then locks the cache, validates the actor exists, and returns the value 44 | // it does this to make sure that the statement of BadActor is kept intact 45 | func (d *Director) ActorsName(a *Actor) (string, error) { 46 | d.Lock() 47 | defer d.Unlock() 48 | 49 | if !d.actorExists(a.Name()) { 50 | return "", fmt.Errorf("Actor [%s] isn't found in cache.", a.Name()) 51 | } 52 | 53 | n := a.Name() 54 | return n, nil 55 | } 56 | 57 | func (d *Director) lInfraction(an string, rn string) error { 58 | d.Lock() 59 | defer d.Unlock() 60 | 61 | if !d.ruleExists(rn) { 62 | return fmt.Errorf("Rule [%v] does not exists", rn) 63 | } 64 | 65 | if !d.actorExists(an) { 66 | d.createActor(an, rn) 67 | d.deleteOldest() 68 | } 69 | 70 | // bail if actor is already jailed 71 | if d.isJailedFor(an, rn) { 72 | return fmt.Errorf("Actor [%v] is already jailed for [%v]", an, rn) 73 | } 74 | 75 | // move the infraction up the stack 76 | // else create infraction if needed 77 | if d.infractionExists(an, rn) { 78 | d.up(an) 79 | } else { 80 | d.createInfraction(an, rn) 81 | } 82 | 83 | return d.incrementInfraction(an, rn) 84 | } 85 | 86 | func (d *Director) lCreateInfraction(an string, rn string) error { 87 | d.Lock() 88 | defer d.Unlock() 89 | 90 | if !d.actorExists(an) { 91 | return fmt.Errorf("director.CreateInfraction() failed, Actor does not exists") 92 | } 93 | return d.createInfraction(an, rn) 94 | } 95 | 96 | func (d *Director) lTimeToLive(an string) (time.Time, error) { 97 | d.Lock() 98 | defer d.Unlock() 99 | 100 | if !d.actorExists(an) { 101 | return time.Now(), fmt.Errorf("director.CreateInfraction() failed, Actor does not exists") 102 | } 103 | return d.timeToLive(an), nil 104 | } 105 | 106 | func (d *Director) lCreateActor(an string, rn string) error { 107 | d.Lock() 108 | defer d.Unlock() 109 | 110 | if d.actorExists(an) { 111 | return fmt.Errorf("director.CreateActor() failed, Actor already exists") 112 | } 113 | return d.createActor(an, rn) 114 | } 115 | 116 | func (d *Director) lKeepAlive(an string) error { 117 | d.Lock() 118 | defer d.Unlock() 119 | if !d.actorExists(an) { 120 | return fmt.Errorf("director.KeepAlive() failed, Actor does not exists") 121 | } 122 | d.keepAlive(an) 123 | return nil 124 | } 125 | 126 | func (d *Director) lActorExists(an string) bool { 127 | d.Lock() 128 | defer d.Unlock() 129 | return d.actorExists(an) 130 | } 131 | 132 | func (d *Director) lInfractionExists(an string, rn string) bool { 133 | d.Lock() 134 | defer d.Unlock() 135 | 136 | if !d.actorExists(an) { 137 | return false 138 | } 139 | return d.infractionExists(an, rn) 140 | } 141 | 142 | func (d *Director) lIsJailedFor(an string, rn string) bool { 143 | d.Lock() 144 | defer d.Unlock() 145 | 146 | if !d.actorExists(an) { 147 | return false 148 | } 149 | return d.isJailedFor(an, rn) 150 | } 151 | 152 | func (d *Director) lIsJailed(an string) bool { 153 | d.Lock() 154 | defer d.Unlock() 155 | if !d.actorExists(an) { 156 | return false 157 | } 158 | return d.isJailed(an) 159 | } 160 | 161 | func (d *Director) lStrikes(an string, rn string) (int, error) { 162 | d.Lock() 163 | defer d.Unlock() 164 | 165 | if !d.actorExists(an) { 166 | return 0, fmt.Errorf("director.Strikes() failed, Actor does not exists") 167 | } 168 | 169 | if !d.infractionExists(an, rn) { 170 | return 0, fmt.Errorf("director.Strikes() failed, Infraction does not exists") 171 | } 172 | 173 | return d.strikes(an, rn), nil 174 | } 175 | 176 | func (d *Director) lAddRule(r *Rule) error { 177 | d.Lock() 178 | defer d.Unlock() 179 | 180 | if d.ruleExists(r.Name) { 181 | return fmt.Errorf("AddRule failed, Rule [%s] already exists", r.Name) 182 | } 183 | 184 | // add the rule 185 | d.rules[r.Name] = r 186 | 187 | return nil 188 | } 189 | 190 | // 191 | // NO LOCKS - UNSAFE - BECAREFUL 192 | // 193 | // below this point are helper functions that are dependant on the calling functions to preform the appropriate locking 194 | 195 | func (d *Director) maintenance(an string) { 196 | if av := d.actors[an]; av != nil { 197 | a := av.Value.(*Actor) 198 | 199 | for _, j := range a.jails { 200 | a.timeServed(j) 201 | } 202 | for _, inf := range a.infractions { 203 | a.expire(inf.rule.Name) 204 | } 205 | if a.shouldDelete() { 206 | delete(d.actors, a.name) 207 | d.size-- 208 | } 209 | } 210 | } 211 | 212 | func (d *Director) createActor(an string, rn string) error { 213 | 214 | if !d.ruleExists(rn) { 215 | return fmt.Errorf("createActor failed for Actor [%s], Rule [%s] does not exist", an, rn) 216 | } 217 | 218 | a := newActor(an, d) 219 | e := d.index.PushFront(a) 220 | d.actors[an] = e 221 | d.size++ 222 | 223 | return nil 224 | } 225 | 226 | func (d *Director) deleteOldest() { 227 | for d.isFull() { 228 | e := d.index.Back() 229 | a := e.Value.(*Actor) 230 | d.index.Remove(e) 231 | delete(d.actors, a.name) 232 | d.size-- 233 | } 234 | } 235 | 236 | func (d *Director) isFull() bool { 237 | if d.size > d.capacity { 238 | return true 239 | } 240 | return false 241 | } 242 | 243 | func (d *Director) ruleExists(rn string) bool { 244 | _, ok := d.rules[rn] 245 | return ok 246 | } 247 | 248 | func (d *Director) actorExists(an string) bool { 249 | if _, ok := d.actors[an]; ok { 250 | d.maintenance(an) 251 | } 252 | _, ok := d.actors[an] 253 | return ok 254 | } 255 | 256 | func (d *Director) up(an string) { 257 | if a := d.actors[an]; a != nil { 258 | a.Value.(*Actor).accessedAt = time.Now() 259 | d.index.MoveToFront(a) 260 | d.deleteOldest() 261 | } 262 | } 263 | 264 | func (d *Director) incrementInfraction(an string, rn string) error { 265 | return d.actors[an].Value.(*Actor).infraction(rn) 266 | } 267 | 268 | func (d *Director) createInfraction(an string, rn string) error { 269 | inf := newInfraction(d.rules[rn]) 270 | return d.actors[an].Value.(*Actor).createInfraction(inf) 271 | } 272 | 273 | func (d *Director) infractionExists(an string, rn string) bool { 274 | return d.actors[an].Value.(*Actor).infractionExists(rn) 275 | } 276 | 277 | func (d *Director) isJailed(an string) bool { 278 | return d.actors[an].Value.(*Actor).isJailed() 279 | } 280 | 281 | func (d *Director) isJailedFor(an string, rn string) bool { 282 | return d.actors[an].Value.(*Actor).isJailedFor(rn) 283 | } 284 | 285 | func (d *Director) strikes(an string, rn string) int { 286 | return d.actors[an].Value.(*Actor).strikes(rn) 287 | } 288 | 289 | func (d *Director) timeToLive(an string) time.Time { 290 | return d.actors[an].Value.(*Actor).timeToLive() 291 | } 292 | 293 | func (d *Director) keepAlive(an string) { 294 | d.actors[an].Value.(*Actor).rebaseAll() 295 | } 296 | 297 | func (d *Director) actor(an string) *Actor { 298 | return d.actors[an].Value.(*Actor) 299 | } 300 | -------------------------------------------------------------------------------- /director_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestMaintenanceWhenActorDoesntExist(t *testing.T) { 10 | d := NewDirector(ia) 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("director.maintenance() did panic") 14 | } 15 | }() 16 | 17 | d.maintenance("this is a key of an actor that does not exist") 18 | } 19 | 20 | func TestDirectorlMaintenance(t *testing.T) { 21 | var b bool 22 | var err error 23 | d := NewDirector(ia) 24 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 25 | r1n := "r1" 26 | r2n := "r2" 27 | r1 := &Rule{ 28 | Name: r1n, 29 | Message: "r1 message", 30 | StrikeLimit: 3, 31 | ExpireBase: time.Minute * 1, 32 | Sentence: time.Minute * 5, 33 | } 34 | 35 | r2 := &Rule{ 36 | Name: r2n, 37 | Message: "r2 message", 38 | StrikeLimit: 3, 39 | ExpireBase: time.Minute * 1, 40 | Sentence: time.Minute * 5, 41 | } 42 | 43 | d.lAddRule(r1) 44 | d.lAddRule(r2) 45 | 46 | if d.size != 0 { 47 | t.Errorf("d.size should be 0, instead [%v]", d.size) 48 | } 49 | 50 | for i := 0; i < 3; i++ { 51 | err = d.lInfraction(an, r1n) 52 | if err != nil { 53 | t.Errorf("lIsJailedFor lInfraction should not error [%v]", err) 54 | } 55 | } 56 | 57 | for i := 0; i < 1; i++ { 58 | err = d.lInfraction(an, r2n) 59 | if err != nil { 60 | t.Errorf("lIsJailedFor lInfraction should not error [%v]", err) 61 | } 62 | } 63 | 64 | if d.size != 1 { 65 | t.Errorf("d.size should be 1, instead [%v]", d.size) 66 | } 67 | 68 | b = d.lIsJailed(an) 69 | if !b { 70 | t.Errorf("lIsJailed should be true instead [%v]", b) 71 | } 72 | 73 | b = d.lIsJailedFor(an, r1n) 74 | if !b { 75 | t.Errorf("lIsJailedFor should be true instead [%v]", b) 76 | } 77 | 78 | // MOCK STATE CHANGE 79 | // Remove 10 minutes from the world 80 | // get actor 81 | a := d.actor(an) 82 | // set time to be one hour ago 83 | dur := time.Now().Add(-time.Hour * 1) 84 | 85 | // change time.Time of infraction and jail 86 | a.jails[r1n].releaseBy = dur 87 | a.infractions[r2n].expireBy = dur 88 | 89 | // perform maintenance 90 | d.lMaintenance() 91 | 92 | // change time to live 93 | a.ttl = dur 94 | 95 | // perform maintenance 96 | d.lMaintenance() 97 | 98 | b = d.lInfractionExists(an, r2n) 99 | if b { 100 | t.Errorf("lInfractionExists should be false instead [%v]", b) 101 | } 102 | 103 | b = d.lIsJailed(an) 104 | if b { 105 | t.Errorf("lIsJailed should be false instead [%v]", b) 106 | } 107 | 108 | b = d.lIsJailedFor(an, r1n) 109 | if b { 110 | t.Errorf("lIsJailedFor should be false instead [%v]", b) 111 | } 112 | 113 | if d.size != 0 { 114 | t.Errorf("d.size should be 0, instead [%v]", d.size) 115 | } 116 | 117 | } 118 | 119 | func TestActorExists(t *testing.T) { 120 | var err error 121 | d := NewDirector(ia) 122 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 123 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 124 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 125 | r := &Rule{ 126 | Name: rn, 127 | Message: rm, 128 | StrikeLimit: 3, 129 | ExpireBase: time.Millisecond * 10, 130 | Sentence: time.Millisecond * 10, 131 | } 132 | 133 | err = d.lAddRule(r) 134 | if err != nil { 135 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 136 | } 137 | 138 | if d.lActorExists(an) { 139 | t.Errorf("Actor [%s] should not be found", an) 140 | } 141 | 142 | err = d.lCreateActor(an, rn) 143 | if err != nil { 144 | t.Errorf("Actor [%s] should be created %v", an, err) 145 | } 146 | 147 | if !d.lActorExists(an) { 148 | t.Errorf("Actor [%s] should be found", an) 149 | } 150 | } 151 | 152 | func TestKeepAlive(t *testing.T) { 153 | var err error 154 | d := NewDirector(ia) 155 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 156 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 157 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 158 | r := &Rule{ 159 | Name: rn, 160 | Message: rm, 161 | StrikeLimit: 3, 162 | ExpireBase: time.Second * 10, 163 | Sentence: time.Second * 10, 164 | } 165 | 166 | err = d.lAddRule(r) 167 | if err != nil { 168 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 169 | } 170 | 171 | err = d.lKeepAlive(an) 172 | if err == nil { 173 | t.Errorf("Keep Alive for Actor [%s] should fail", an) 174 | } 175 | 176 | err = d.lCreateActor(an, rn) 177 | if err != nil { 178 | t.Errorf("lCreateActor for Actor [%s] should not fail", an) 179 | } 180 | 181 | err = d.lKeepAlive(an) 182 | if err != nil { 183 | t.Errorf("Keep Alive should not fail : %v ", err) 184 | } 185 | } 186 | 187 | func TestStrikes(t *testing.T) { 188 | var i int 189 | var err error 190 | d := NewDirector(ia) 191 | ban := "badname" 192 | brn := "badrule" 193 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 194 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 195 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 196 | r := &Rule{ 197 | Name: rn, 198 | Message: rm, 199 | StrikeLimit: 3, 200 | ExpireBase: time.Minute * 10, 201 | Sentence: time.Minute * 10, 202 | } 203 | 204 | err = d.lAddRule(r) 205 | if err != nil { 206 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 207 | } 208 | 209 | // setup valid lInfraction 210 | err = d.lInfraction(an, rn) 211 | if err != nil { 212 | t.Errorf("Ifraction should not fail : %v ", err) 213 | } 214 | 215 | i, err = d.lStrikes(ban, brn) 216 | if err == nil { 217 | t.Errorf("lStrikes should fail : %v ", err) 218 | } 219 | 220 | if i != 0 { 221 | t.Errorf("lStrikes should be [%v]:[%v] ", 0, err) 222 | } 223 | 224 | i, err = d.lStrikes(an, brn) 225 | if err == nil { 226 | t.Errorf("lStrikes should fail : %v ", err) 227 | } 228 | 229 | if i != 0 { 230 | t.Errorf("lStrikes should be [%v]:[%v] ", 0, err) 231 | } 232 | 233 | i, err = d.lStrikes(an, rn) 234 | if err != nil { 235 | t.Errorf("lStrikes should not fail : %v ", err) 236 | } 237 | 238 | if i != 1 { 239 | t.Errorf("lStrikes should be [%v]:[%v] ", 1, err) 240 | } 241 | 242 | } 243 | 244 | func TestInfraction(t *testing.T) { 245 | var err error 246 | d := NewDirector(ia) 247 | ban := "badname" 248 | brn := "badrule" 249 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 250 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 251 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 252 | r := &Rule{ 253 | Name: rn, 254 | Message: rm, 255 | StrikeLimit: 3, 256 | ExpireBase: time.Minute * 10, 257 | Sentence: time.Minute * 10, 258 | } 259 | 260 | err = d.lAddRule(r) 261 | if err != nil { 262 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 263 | } 264 | 265 | err = d.lInfraction(ban, brn) 266 | if err == nil { 267 | t.Errorf("Ifraction should fail : %v ", err) 268 | } 269 | 270 | err = d.lInfraction(an, brn) 271 | if err == nil { 272 | t.Errorf("Ifraction should fail : %v ", err) 273 | } 274 | 275 | err = d.lInfraction(an, rn) 276 | if err != nil { 277 | t.Errorf("Ifraction should not fail : %v ", err) 278 | } 279 | 280 | i, err := d.lStrikes(an, rn) 281 | if err != nil { 282 | t.Errorf("lStrikes should not fail : %v ", err) 283 | } 284 | 285 | if i != 1 { 286 | t.Errorf("lStrikes return value is %d should equal %d : %v ", i, 1, err) 287 | } 288 | 289 | } 290 | 291 | func TestInfractionIncrement(t *testing.T) { 292 | var err error 293 | d := NewDirector(ia) 294 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 295 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 296 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 297 | r := &Rule{ 298 | Name: rn, 299 | Message: rm, 300 | StrikeLimit: 3, 301 | ExpireBase: time.Millisecond * 10, 302 | Sentence: time.Millisecond * 10, 303 | } 304 | 305 | err = d.lAddRule(r) 306 | if err != nil { 307 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 308 | } 309 | 310 | err = d.lInfraction(an, rn) 311 | if err != nil { 312 | t.Errorf("lInfraction should not be err : %v", err) 313 | } 314 | 315 | i, err := d.lStrikes(an, rn) 316 | if err != nil { 317 | t.Errorf("lStrikes should no fail : %v ", err) 318 | } 319 | 320 | if i != 1 { 321 | t.Errorf("lStrikes should return %d instead %d", 1, i) 322 | } 323 | 324 | } 325 | 326 | func TestIsJailedFor(t *testing.T) { 327 | var b bool 328 | var err error 329 | expectFalse := false 330 | expectTrue := true 331 | d := NewDirector(ia) 332 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 333 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 334 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 335 | r := &Rule{ 336 | Name: rn, 337 | Message: rm, 338 | StrikeLimit: 3, 339 | ExpireBase: time.Millisecond * 10, 340 | Sentence: time.Millisecond * 10, 341 | } 342 | 343 | err = d.lAddRule(r) 344 | if err != nil { 345 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 346 | } 347 | 348 | b = d.lIsJailedFor(an, rn) 349 | if b != expectFalse { 350 | t.Errorf("lIsJailedFor() should be [%v] instead %v", expectFalse, b) 351 | } 352 | 353 | for i := 0; i < 3; i++ { 354 | err = d.lInfraction(an, rn) 355 | if err != nil { 356 | t.Errorf("lIsJailedFor() lInfraction should not error [%v]", err) 357 | } 358 | } 359 | 360 | b = d.lIsJailedFor(an, rn) 361 | if b != expectTrue { 362 | t.Errorf("lIsJailedFor() should be [%v] instead %v", expectTrue, b) 363 | } 364 | 365 | } 366 | 367 | func TestIsJailed(t *testing.T) { 368 | var b bool 369 | var err error 370 | expectFalse := false 371 | expectTrue := true 372 | d := NewDirector(ia) 373 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 374 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 375 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 376 | r := &Rule{ 377 | Name: rn, 378 | Message: rm, 379 | StrikeLimit: 3, 380 | ExpireBase: time.Millisecond * 10, 381 | Sentence: time.Millisecond * 10, 382 | } 383 | 384 | err = d.lAddRule(r) 385 | if err != nil { 386 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 387 | } 388 | 389 | b = d.lIsJailed(an) 390 | if b != expectFalse { 391 | t.Errorf("lIsJailed should be should be [%v] instead %v", expectFalse, b) 392 | } 393 | 394 | for i := 0; i < 3; i++ { 395 | err = d.lInfraction(an, rn) 396 | if err != nil { 397 | t.Errorf("lIsJailedFor lInfraction should not error [%v]", err) 398 | } 399 | } 400 | 401 | b = d.lIsJailed(an) 402 | if b != expectTrue { 403 | t.Errorf("lIsJailed should be should be [%v] instead %v", expectTrue, b) 404 | } 405 | 406 | } 407 | 408 | func TestInfractionExists(t *testing.T) { 409 | var b bool 410 | var err error 411 | expectFalse := false 412 | expectTrue := true 413 | d := NewDirector(ia) 414 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 415 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 416 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 417 | r := &Rule{ 418 | Name: rn, 419 | Message: rm, 420 | StrikeLimit: 3, 421 | ExpireBase: time.Millisecond * 10, 422 | Sentence: time.Millisecond * 10, 423 | } 424 | err = d.lAddRule(r) 425 | if err != nil { 426 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 427 | } 428 | 429 | b = d.lInfractionExists(an, rn) 430 | if b != expectFalse { 431 | t.Errorf("lInfraction should not exist: expected %v instead %v", expectFalse, b) 432 | } 433 | 434 | d.lInfraction(an, rn) 435 | b = d.lInfractionExists(an, rn) 436 | if b != expectTrue { 437 | t.Errorf("lInfraction should exist: expected %v instead %v", expectTrue, b) 438 | } 439 | 440 | } 441 | 442 | func TestCreateInfraction(t *testing.T) { 443 | var err error 444 | d := NewDirector(ia) 445 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 446 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 447 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 448 | r := &Rule{ 449 | Name: rn, 450 | Message: rm, 451 | StrikeLimit: 3, 452 | ExpireBase: time.Millisecond * 10, 453 | Sentence: time.Millisecond * 10, 454 | } 455 | err = d.lAddRule(r) 456 | if err != nil { 457 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 458 | } 459 | 460 | br := "badrule" 461 | ba := "badactor" 462 | 463 | err = d.lCreateInfraction(an, br) 464 | if err == nil { 465 | t.Errorf("Should error, Rule does not exist: %v", err) 466 | } 467 | 468 | err = d.lCreateInfraction(ba, rn) 469 | if err == nil { 470 | t.Errorf("Should error, Actor does not exist: %v", err) 471 | } 472 | 473 | err = d.lCreateActor(an, rn) 474 | if err != nil { 475 | t.Errorf("Should not error, Actor and Rule exist: %v", err) 476 | } 477 | 478 | err = d.lCreateInfraction(an, rn) 479 | if err != nil { 480 | t.Errorf("Should not error, Actor and Rule exist: %v", err) 481 | } 482 | 483 | } 484 | 485 | func TestAddRule(t *testing.T) { 486 | var err error 487 | d := NewDirector(ia) 488 | 489 | r := NewClassicRule("PasswordReset", "You have requested a password reset too often") 490 | 491 | err = d.lAddRule(r) 492 | if err != nil { 493 | t.Errorf("Should not fail, Rule shouldn't exist: %v", err) 494 | } 495 | 496 | err = d.lAddRule(r) 497 | if err == nil { 498 | t.Errorf("Should fail, Rule does exist: %v", err) 499 | } 500 | 501 | } 502 | 503 | func TestDirectorlIsJailedFor(t *testing.T) { 504 | var b bool 505 | var err error 506 | d := NewDirector(ia) 507 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 508 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 509 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 510 | r := &Rule{ 511 | Name: rn, 512 | Message: rm, 513 | StrikeLimit: 3, 514 | ExpireBase: time.Second * 60, 515 | Sentence: time.Millisecond * 10, 516 | } 517 | d.lAddRule(r) 518 | 519 | if d.size != 0 { 520 | t.Errorf("d.size should be 0, instead [%v]", d.size) 521 | } 522 | 523 | for i := 0; i < 3; i++ { 524 | err = d.lInfraction(an, rn) 525 | if err != nil { 526 | t.Errorf("lIsJailedFor lInfraction should not error [%v]", err) 527 | } 528 | } 529 | 530 | if d.size != 1 { 531 | t.Errorf("d.size should be 1, instead [%v]", d.size) 532 | } 533 | 534 | b = d.lIsJailed(an) 535 | if !b { 536 | t.Errorf("lIsJailed should be true instead [%v]", b) 537 | } 538 | 539 | b = d.lIsJailedFor(an, rn) 540 | if !b { 541 | t.Errorf("lIsJailedFor should be true instead [%v]", b) 542 | } 543 | 544 | // MOCK STATE CHANGE 545 | // Remove 5 minutes from the world 546 | a := d.actors[an].Value.(*Actor) 547 | dur := time.Now().Add(-time.Minute * 5) 548 | a.jails[rn].releaseBy = dur 549 | a.ttl = dur 550 | 551 | d.lMaintenance() 552 | 553 | b = d.lIsJailed(an) 554 | if b { 555 | t.Errorf("lIsJailed should be false instead [%v]", b) 556 | } 557 | 558 | b = d.lIsJailedFor(an, rn) 559 | if b { 560 | t.Errorf("lIsJailedFor should be false instead [%v]", b) 561 | } 562 | 563 | } 564 | 565 | func TestCreateActor(t *testing.T) { 566 | var err error 567 | d := NewDirector(ia) 568 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 569 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 570 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 571 | r := &Rule{ 572 | Name: rn, 573 | Message: rm, 574 | StrikeLimit: 3, 575 | ExpireBase: time.Second * 10, 576 | Sentence: time.Second * 10, 577 | } 578 | 579 | err = d.lCreateActor(an, rn) 580 | if err == nil { 581 | t.Errorf("Actor [%s] should be created %v", an, err) 582 | } 583 | 584 | err = d.lAddRule(r) 585 | if err != nil { 586 | t.Errorf("lAddRule for Actor [%s] should not fail", an) 587 | } 588 | 589 | err = d.lCreateActor(an, rn) 590 | if err != nil { 591 | t.Errorf("director.lCreateActor() for [%s] should not fail", an) 592 | } 593 | 594 | err = d.lCreateActor(an, rn) 595 | if err == nil { 596 | t.Errorf("director.lCreateActor() for [%s] should fail", an) 597 | } 598 | } 599 | 600 | func TestDeleteOldest(t *testing.T) { 601 | var ok bool 602 | var err error 603 | var size int32 604 | var i int64 605 | size = 10 606 | d := NewDirector(size) 607 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 608 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 609 | r := &Rule{ 610 | Name: rn, 611 | Message: rm, 612 | StrikeLimit: 3, 613 | ExpireBase: time.Minute * 10, 614 | Sentence: time.Minute * 10, 615 | } 616 | 617 | err = d.lAddRule(r) 618 | if err != nil { 619 | t.Errorf("lAddRule [%s] should not fail", rn) 620 | } 621 | 622 | ok = d.isFull() 623 | if ok == true { 624 | t.Errorf("isFull want [false] has [%v]", ok) 625 | } 626 | 627 | for i = 0; i < 20; i++ { 628 | an := strconv.FormatInt(i, 10) 629 | d.createActor(an, rn) 630 | } 631 | 632 | ok = d.isFull() 633 | if ok == false { 634 | t.Errorf("isFull want [true] has [%v]", ok) 635 | } 636 | 637 | d.deleteOldest() 638 | 639 | ok = d.isFull() 640 | if ok == true { 641 | t.Errorf("isFull want [false] has [%v]", ok) 642 | } 643 | 644 | } 645 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jaredfolkins/badactor 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /infraction.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import "time" 4 | 5 | type infraction struct { 6 | rule *Rule 7 | strikes int 8 | expireBy time.Time 9 | } 10 | 11 | func newInfraction(r *Rule) *infraction { 12 | return &infraction{ 13 | rule: r, 14 | strikes: 0, 15 | expireBy: time.Now().Add(r.ExpireBase), 16 | } 17 | } 18 | 19 | func (inf *infraction) rebase() { 20 | inf.expireBy = time.Now().Add(inf.rule.ExpireBase) 21 | } 22 | -------------------------------------------------------------------------------- /infraction_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewInfraction(t *testing.T) { 10 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 11 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 12 | sl := 3 13 | eb := time.Second * 60 14 | s := time.Second * 60 15 | r := &Rule{ 16 | Name: rn, 17 | Message: rm, 18 | StrikeLimit: sl, 19 | ExpireBase: eb, 20 | Sentence: s, 21 | } 22 | 23 | inf := newInfraction(r) 24 | 25 | if inf.strikes != 0 { 26 | t.Errorf("Infraction.Strikes should be [%v]", 0) 27 | } 28 | 29 | } 30 | 31 | func TestInfractionRebase(t *testing.T) { 32 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 33 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 34 | sl := 3 35 | eb := time.Second * 60 36 | s := time.Second * 60 37 | r := &Rule{ 38 | Name: rn, 39 | Message: rm, 40 | StrikeLimit: sl, 41 | ExpireBase: eb, 42 | Sentence: s, 43 | } 44 | 45 | inf := newInfraction(r) 46 | ot := inf.expireBy 47 | inf.rebase() 48 | if !inf.expireBy.After(ot) { 49 | t.Errorf("Infraction.ExpireBy should be new, greater value, instead [%v:%v]", inf.expireBy, ot) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /jail.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import "time" 4 | 5 | type jail struct { 6 | rule *Rule 7 | releaseBy time.Time 8 | start time.Time 9 | } 10 | 11 | func newJail(r *Rule, sen time.Duration) *jail { 12 | return &jail{ 13 | rule: r, 14 | releaseBy: time.Now().Add(sen), 15 | start: time.Now(), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jail_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | //placeholder 4 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | type message struct { 4 | reaperAlive bool 5 | } 6 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | //placeholder 4 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import "time" 4 | 5 | // Rule struct is used as a basic ruleset to Judge and Jail an Actor by 6 | type Rule struct { 7 | Name string 8 | Message string 9 | StrikeLimit int 10 | ExpireBase time.Duration 11 | Sentence time.Duration 12 | Action Action 13 | } 14 | 15 | // NewClassicRule returns a Rule with basic default values 16 | func NewClassicRule(n string, m string) *Rule { 17 | return &Rule{ 18 | Name: n, 19 | Message: m, 20 | StrikeLimit: 10, 21 | ExpireBase: time.Minute * 10, 22 | Sentence: time.Minute * 10, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rule_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | //placeholder 4 | -------------------------------------------------------------------------------- /status.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | type status struct { 4 | outgoing chan *message 5 | } 6 | 7 | func newStatus() *status { 8 | return &status{ 9 | outgoing: make(chan *message), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /status_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | -------------------------------------------------------------------------------- /studio.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "hash/fnv" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Studio is the singleton instance, it contains the Directors(buckets) who have many Actors(points) 10 | type Studio struct { 11 | sync.Mutex 12 | capacity int32 13 | directors map[int32]*Director 14 | rules map[string]*Rule 15 | status chan *status 16 | } 17 | 18 | // NewStudio returns a init'd Studio struct, you pass it an int32 value which is the capacity and informs the Studio how many Directors will be created, it is also the value that jumpHash uses to mod 19 | func NewStudio(md int32) *Studio { 20 | return &Studio{ 21 | capacity: md, 22 | directors: make(map[int32]*Director, md), 23 | rules: make(map[string]*Rule), 24 | status: make(chan *status), 25 | } 26 | } 27 | 28 | // AddRule accepts a Rule struct and adds it to the rules map if it doesn't exist 29 | func (st *Studio) AddRule(r *Rule) { 30 | st.Lock() 31 | st.rules[r.Name] = r 32 | st.Unlock() 33 | return 34 | } 35 | 36 | // ApplyRules takes the currently stored rules map and applies it to all Directors 37 | func (st *Studio) ApplyRules() error { 38 | for _, d := range st.directors { 39 | for _, r := range st.rules { 40 | d.lAddRule(r) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | // CreateDirectors creates and adds the Directors to the director map 47 | func (st *Studio) CreateDirectors(ma int32) error { 48 | var dk int32 49 | for dk = 0; dk < st.capacity; dk++ { 50 | d := NewDirector(ma) 51 | st.directors[dk] = d 52 | } 53 | return st.ApplyRules() 54 | } 55 | 56 | // Infraction accepts an ActorName and RuleName and either creates, increments, or increments and jails the Actor 57 | func (st *Studio) Infraction(an string, rn string) error { 58 | d := st.Director(an) 59 | return d.lInfraction(an, rn) 60 | } 61 | 62 | // Strikes accepts an ActorName and a RuleName and returns the total strikes an Actor holds for a particular Rule 63 | func (st *Studio) Strikes(an string, rn string) (int, error) { 64 | d := st.Director(an) 65 | return d.lStrikes(an, rn) 66 | } 67 | 68 | // CreateInfraction takes an ActorName and RuleName and creates an Infraction 69 | func (st *Studio) CreateInfraction(an string, rn string) error { 70 | d := st.Director(an) 71 | return d.lCreateInfraction(an, rn) 72 | } 73 | 74 | // CreateActor takes an ActorName and RuleName and creates an Actor 75 | func (st *Studio) CreateActor(an string, rn string) error { 76 | d := st.Director(an) 77 | return d.lCreateActor(an, rn) 78 | } 79 | 80 | // KeepAlive accepts an ActorName and allows you to rebase the TTL for the Actor so that it isn't removed from the stack as scheduled, keeping it alive. 81 | func (st *Studio) KeepAlive(an string) error { 82 | d := st.Director(an) 83 | return d.lKeepAlive(an) 84 | } 85 | 86 | // ActorExists accepts an ActorName and returns a bool if the Actor is found 87 | func (st *Studio) ActorExists(an string) bool { 88 | d := st.Director(an) 89 | return d.lActorExists(an) 90 | } 91 | 92 | // InfractionExists accepts an ActorName and RuleName and returns a bool if the Infraction is found 93 | func (st *Studio) InfractionExists(an string, rn string) bool { 94 | d := st.Director(an) 95 | return d.lInfractionExists(an, rn) 96 | } 97 | 98 | // IsJailedFor accepts an ActorName and a RuleName and returns a bool if the Actor is Jailed for that particular Rule 99 | func (st *Studio) IsJailedFor(an string, rn string) bool { 100 | d := st.Director(an) 101 | return d.lIsJailedFor(an, rn) 102 | } 103 | 104 | // IsJailed accepts an ActorName and returns a bool if the Actor is Jailed for ANY Rule 105 | func (st *Studio) IsJailed(an string) bool { 106 | d := st.Director(an) 107 | return d.lIsJailed(an) 108 | } 109 | 110 | // StartReaper starts the reaping goroutine and takes a time.Duration on how often you want the Reaper to run 111 | func (st *Studio) StartReaper(dur time.Duration) { 112 | ticker := time.NewTicker(dur) 113 | go func() { 114 | for { 115 | select { 116 | case <-ticker.C: 117 | for _, d := range st.directors { 118 | d.lMaintenance() 119 | } 120 | case stat := <-st.status: 121 | m := &message{ 122 | reaperAlive: true, 123 | } 124 | stat.outgoing <- m 125 | } 126 | } 127 | }() 128 | } 129 | 130 | func (st *Studio) Status() *message { 131 | stat := newStatus() 132 | defer close(stat.outgoing) 133 | st.status <- stat 134 | return <-stat.outgoing 135 | } 136 | 137 | // Director takes the name of an Actor as a string, serializes it, uses the jumpHash aglo to determine the Director that the Actor belongs to 138 | func (st Studio) Director(an string) *Director { 139 | dk := st.jumpHash(st.serialize(an), st.capacity) 140 | return st.directors[dk] 141 | } 142 | 143 | // serialize a string to an uint64 144 | func (st Studio) serialize(s string) uint64 { 145 | h := fnv.New64a() 146 | h.Write([]byte(s)) 147 | return h.Sum64() 148 | } 149 | 150 | func (st Studio) jumpHash(key uint64, numBuckets int32) int32 { 151 | 152 | var b int64 = -1 153 | var j int64 154 | 155 | for j < int64(numBuckets) { 156 | b = j 157 | key = key*2862933555777941757 + 1 158 | j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1))) 159 | } 160 | return int32(b) 161 | } 162 | -------------------------------------------------------------------------------- /studio_bench_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func BenchmarkStudioInfraction512(b *testing.B) { 12 | st := NewStudio(512) 13 | 14 | rn := "Login" 15 | r := &Rule{ 16 | Name: rn, 17 | Message: "Failed to login too many times", 18 | StrikeLimit: 3, 19 | ExpireBase: time.Second * 2, 20 | Sentence: time.Second * 2, 21 | } 22 | 23 | st.AddRule(r) 24 | 25 | err := st.CreateDirectors(1024) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | dur := time.Minute * time.Duration(60) 31 | st.StartReaper(dur) 32 | 33 | for i := 0; i < b.N; i++ { 34 | an := strconv.FormatInt(rand.Int63(), 10) 35 | st.Infraction(an, rn) 36 | } 37 | } 38 | 39 | func BenchmarkStudioInfraction1024(b *testing.B) { 40 | st := NewStudio(1024) 41 | 42 | rn := "Login" 43 | r := &Rule{ 44 | Name: rn, 45 | Message: "Failed to login too many times", 46 | StrikeLimit: 3, 47 | ExpireBase: time.Second * 2, 48 | Sentence: time.Second * 2, 49 | } 50 | 51 | st.AddRule(r) 52 | 53 | err := st.CreateDirectors(1024) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | dur := time.Minute * time.Duration(60) 59 | st.StartReaper(dur) 60 | 61 | for i := 0; i < b.N; i++ { 62 | an := strconv.FormatInt(rand.Int63(), 10) 63 | st.Infraction(an, rn) 64 | } 65 | } 66 | 67 | func BenchmarkStudioInfraction2048(b *testing.B) { 68 | st := NewStudio(2048) 69 | 70 | rn := "Login" 71 | r := &Rule{ 72 | Name: rn, 73 | Message: "Failed to login too many times", 74 | StrikeLimit: 3, 75 | ExpireBase: time.Second * 2, 76 | Sentence: time.Second * 2, 77 | } 78 | 79 | st.AddRule(r) 80 | 81 | err := st.CreateDirectors(1024) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | dur := time.Minute * time.Duration(60) 87 | st.StartReaper(dur) 88 | 89 | for i := 0; i < b.N; i++ { 90 | an := strconv.FormatInt(rand.Int63(), 10) 91 | st.Infraction(an, rn) 92 | } 93 | } 94 | 95 | func BenchmarkStudioInfraction4096(b *testing.B) { 96 | st := NewStudio(4096) 97 | 98 | rn := "Login" 99 | r := &Rule{ 100 | Name: rn, 101 | Message: "Failed to login too many times", 102 | StrikeLimit: 3, 103 | ExpireBase: time.Second * 2, 104 | Sentence: time.Second * 2, 105 | } 106 | 107 | st.AddRule(r) 108 | 109 | err := st.CreateDirectors(1024) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | 114 | dur := time.Minute * time.Duration(60) 115 | st.StartReaper(dur) 116 | 117 | for i := 0; i < b.N; i++ { 118 | an := strconv.FormatInt(rand.Int63(), 10) 119 | st.Infraction(an, rn) 120 | } 121 | } 122 | 123 | func BenchmarkStudioInfraction65536(b *testing.B) { 124 | st := NewStudio(65536) 125 | 126 | rn := "Login" 127 | r := &Rule{ 128 | Name: rn, 129 | Message: "Failed to login too many times", 130 | StrikeLimit: 3, 131 | ExpireBase: time.Second * 2, 132 | Sentence: time.Second * 2, 133 | } 134 | 135 | st.AddRule(r) 136 | 137 | err := st.CreateDirectors(1024) 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | 142 | dur := time.Minute * time.Duration(60) 143 | st.StartReaper(dur) 144 | 145 | for i := 0; i < b.N; i++ { 146 | an := strconv.FormatInt(rand.Int63(), 10) 147 | st.Infraction(an, rn) 148 | } 149 | } 150 | 151 | func BenchmarkStudioInfraction262144(b *testing.B) { 152 | st := NewStudio(262144) 153 | 154 | rn := "Login" 155 | r := &Rule{ 156 | Name: rn, 157 | Message: "Failed to login too many times", 158 | StrikeLimit: 3, 159 | ExpireBase: time.Second * 2, 160 | Sentence: time.Second * 2, 161 | } 162 | 163 | st.AddRule(r) 164 | 165 | err := st.CreateDirectors(1024) 166 | if err != nil { 167 | log.Fatal(err) 168 | } 169 | 170 | dur := time.Minute * time.Duration(60) 171 | st.StartReaper(dur) 172 | 173 | for i := 0; i < b.N; i++ { 174 | an := strconv.FormatInt(rand.Int63(), 10) 175 | st.Infraction(an, rn) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /studio_test.go: -------------------------------------------------------------------------------- 1 | package badactor 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestStudioStartReaper(t *testing.T) { 10 | 11 | //var b bool 12 | var err error 13 | 14 | st := NewStudio(256) 15 | an := "actorname" 16 | rn := "rulename" 17 | rm := "rulemessage" 18 | 19 | r := &Rule{ 20 | Name: rn, 21 | Message: rm, 22 | StrikeLimit: 3, 23 | ExpireBase: time.Second * 10, 24 | Sentence: time.Minute * 10, 25 | } 26 | 27 | // add rule 28 | st.AddRule(r) 29 | 30 | // creat directors 31 | err = st.CreateDirectors(1024) 32 | if err != nil { 33 | t.Errorf("CreateDirectors failed %v %v", an, rn) 34 | } 35 | 36 | dur := time.Millisecond * time.Duration(1) 37 | st.StartReaper(dur) 38 | 39 | msg := st.Status() 40 | if !msg.reaperAlive { 41 | t.Errorf("isAlive should be true %v", msg.reaperAlive) 42 | } 43 | 44 | msg = st.Status() 45 | if !msg.reaperAlive { 46 | t.Errorf("isAlive should be true %v", msg.reaperAlive) 47 | } 48 | 49 | } 50 | 51 | func TestStudioIsJailedFor(t *testing.T) { 52 | var b bool 53 | var err error 54 | 55 | st := NewStudio(256) 56 | an := "actorname" 57 | rn := "rulename" 58 | rm := "rulemessage" 59 | r := &Rule{ 60 | Name: rn, 61 | Message: rm, 62 | StrikeLimit: 3, 63 | ExpireBase: time.Second * 10, 64 | Sentence: time.Minute * 10, 65 | } 66 | 67 | // add rule 68 | st.AddRule(r) 69 | 70 | // creat directors 71 | err = st.CreateDirectors(1024) 72 | if err != nil { 73 | t.Errorf("CreateDirectors failed %v %v", an, rn) 74 | } 75 | 76 | b = st.IsJailedFor(an, rn) 77 | if b { 78 | t.Errorf("IsJailed should be false, %v %v", an, rn) 79 | } 80 | 81 | for i := 0; i < 3; i++ { 82 | err = st.Infraction(an, rn) 83 | if err != nil { 84 | t.Errorf("Infraction failed %v, %v", an, rn) 85 | } 86 | } 87 | 88 | b = st.IsJailedFor(an, rn) 89 | if !b { 90 | t.Errorf("IsJailed should be true, %v %v", an, rn) 91 | } 92 | 93 | } 94 | 95 | func TestStudioIsJailed(t *testing.T) { 96 | var b bool 97 | var err error 98 | 99 | st := NewStudio(256) 100 | an := "actorname" 101 | rn := "rulename" 102 | rm := "rulemessage" 103 | r := &Rule{ 104 | Name: rn, 105 | Message: rm, 106 | StrikeLimit: 3, 107 | ExpireBase: time.Second * 10, 108 | Sentence: time.Minute * 10, 109 | } 110 | 111 | // add rule 112 | st.AddRule(r) 113 | 114 | // creat directors 115 | err = st.CreateDirectors(1024) 116 | if err != nil { 117 | t.Errorf("CreateDirectors failed %v %v", an, rn) 118 | } 119 | 120 | b = st.IsJailed(an) 121 | if b { 122 | t.Errorf("IsJailed should be false, %v %v", an, rn) 123 | } 124 | 125 | for i := 0; i < 3; i++ { 126 | err = st.Infraction(an, rn) 127 | if err != nil { 128 | t.Errorf("Infraction failed %v, %v", an, rn) 129 | } 130 | } 131 | 132 | b = st.IsJailed(an) 133 | if !b { 134 | t.Errorf("IsJailed should be true, %v %v", an, rn) 135 | } 136 | 137 | } 138 | 139 | func TestStudioKeepAlive(t *testing.T) { 140 | var ti time.Time 141 | var err error 142 | 143 | st := NewStudio(256) 144 | an := "actorname" 145 | rn := "rulename" 146 | rm := "rulemessage" 147 | r := &Rule{ 148 | Name: rn, 149 | Message: rm, 150 | StrikeLimit: 3, 151 | ExpireBase: time.Second * 10, 152 | Sentence: time.Minute * 10, 153 | } 154 | 155 | // add rule 156 | st.AddRule(r) 157 | 158 | // creat directors 159 | err = st.CreateDirectors(1024) 160 | if err != nil { 161 | t.Errorf("CreateDirectors failed %v %v", an, rn) 162 | } 163 | 164 | d := st.Director(an) 165 | ti, err = d.lTimeToLive(an) 166 | if err == nil { 167 | t.Errorf("lTimeToLive() should fail as actor doesn't exist %v, %v, %v", an, err, ti) 168 | } 169 | 170 | err = st.Infraction(an, rn) 171 | if err != nil { 172 | t.Errorf("Infraction failed %v, %v", an, rn) 173 | } 174 | 175 | orig_ttl, err := d.lTimeToLive(an) 176 | if err != nil { 177 | t.Errorf("lTimeToLive() should not fail as actor exists %v, %v, %v", an, err, orig_ttl) 178 | } 179 | 180 | err = st.KeepAlive(an) 181 | if err != nil { 182 | t.Errorf("KeepAlive failed %v, %v", an, rn) 183 | } 184 | 185 | new_ttl, err := d.lTimeToLive(an) 186 | if err != nil { 187 | t.Errorf("lTimeToLive() should not fail as actor exists %v, %v, %v", an, err, orig_ttl) 188 | } 189 | 190 | if new_ttl == orig_ttl { 191 | t.Errorf("KeepAlive() failed to update time %v, %v, %v", an, orig_ttl, new_ttl) 192 | } 193 | 194 | } 195 | 196 | func TestStudioCreateActor(t *testing.T) { 197 | var err error 198 | 199 | st := NewStudio(256) 200 | an := "actorname" 201 | rn := "rulename" 202 | rm := "rulemessage" 203 | r := &Rule{ 204 | Name: rn, 205 | Message: rm, 206 | StrikeLimit: 3, 207 | ExpireBase: time.Second * 10, 208 | Sentence: time.Minute * 10, 209 | } 210 | 211 | // add rule 212 | st.AddRule(r) 213 | 214 | // creat directors 215 | err = st.CreateDirectors(1024) 216 | if err != nil { 217 | t.Errorf("CreateDirectors failed %v %v", an, rn) 218 | } 219 | 220 | if st.ActorExists(an) { 221 | t.Errorf("ActorExists should be false") 222 | } 223 | 224 | err = st.CreateActor(an, rn) 225 | if err != nil { 226 | t.Errorf("CreateActor should be nil [%v]", err) 227 | } 228 | 229 | if !st.ActorExists(an) { 230 | t.Errorf("InfractionExists should be true") 231 | } 232 | } 233 | 234 | func TestStudioCreateInfraction(t *testing.T) { 235 | var err error 236 | 237 | st := NewStudio(256) 238 | an := "actorname" 239 | rn := "rulename" 240 | rm := "rulemessage" 241 | r := &Rule{ 242 | Name: rn, 243 | Message: rm, 244 | StrikeLimit: 3, 245 | ExpireBase: time.Second * 10, 246 | Sentence: time.Minute * 10, 247 | } 248 | 249 | // add rule 250 | st.AddRule(r) 251 | 252 | // creat directors 253 | err = st.CreateDirectors(1024) 254 | if err != nil { 255 | t.Errorf("CreateDirectors failed %v %v", an, rn) 256 | } 257 | 258 | if st.InfractionExists(an, rn) { 259 | t.Errorf("InfractionExists should be false") 260 | } 261 | 262 | err = st.CreateActor(an, rn) 263 | if err != nil { 264 | t.Errorf("CreateActor should be nil [%v]", err) 265 | } 266 | 267 | err = st.CreateInfraction(an, rn) 268 | if err != nil { 269 | t.Errorf("CreateInfraction should be nil [%v]", err) 270 | } 271 | 272 | if !st.InfractionExists(an, rn) { 273 | t.Errorf("InfractionExists should be true") 274 | } 275 | } 276 | 277 | func TestStudioStrikes(t *testing.T) { 278 | var si int 279 | var err error 280 | 281 | st := NewStudio(256) 282 | an := "actorname" 283 | rn := "rulename" 284 | rm := "rulemessage" 285 | r := &Rule{ 286 | Name: rn, 287 | Message: rm, 288 | StrikeLimit: 3, 289 | ExpireBase: time.Second * 10, 290 | Sentence: time.Minute * 10, 291 | } 292 | 293 | // add rule 294 | st.AddRule(r) 295 | 296 | // creat directors 297 | err = st.CreateDirectors(1024) 298 | if err != nil { 299 | t.Errorf("CreateDirectors failed %v %v", an, rn) 300 | } 301 | 302 | si, err = st.Strikes(an, rn) 303 | if si != 0 { 304 | t.Errorf("Strikes for Actor [%v] and Rule [%v] should not be %v, %v", an, rn, si, err) 305 | } 306 | 307 | // 1st inf 308 | err = st.Infraction(an, rn) 309 | if err != nil { 310 | t.Errorf("Infraction failed for Actor [%v] and Rule [%v] should not be %v", an, rn, err) 311 | } 312 | 313 | si, err = st.Strikes(an, rn) 314 | if si != 1 { 315 | t.Errorf("Strikes for Actor [%v] and Rule [%v] should not be %v %v", an, rn, si, err) 316 | } 317 | 318 | // 2nd inf 319 | st.Infraction(an, rn) 320 | si, err = st.Strikes(an, rn) 321 | if si != 2 { 322 | t.Errorf("Strikes for Actor [%v] and Rule [%v] should not be %v %v", an, rn, si, err) 323 | } 324 | 325 | // 3rd inf, jail, strikes for that infraction name should be 0 326 | st.Infraction(an, rn) 327 | si, err = st.Strikes(an, rn) 328 | if si != 0 { 329 | t.Errorf("Strikes for Actor [%v] and Rule [%v] should not be %v %v", an, rn, si, err) 330 | } 331 | 332 | // should still be jailed 333 | st.Infraction(an, rn) 334 | si, err = st.Strikes(an, rn) 335 | if si != 0 { 336 | t.Errorf("Strikes for Actor [%v] and Rule [%v] should not be %v %v", an, rn, si, err) 337 | } 338 | } 339 | 340 | func TestStudioAddRule(t *testing.T) { 341 | st := NewStudio(256) 342 | an := "an_" + strconv.FormatInt(time.Now().UnixNano(), 10) 343 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 344 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 345 | r := &Rule{ 346 | Name: rn, 347 | Message: rm, 348 | StrikeLimit: 3, 349 | ExpireBase: time.Millisecond * 10, 350 | Sentence: time.Millisecond * 10, 351 | } 352 | 353 | // add rule 354 | st.AddRule(r) 355 | 356 | // test rule exists 357 | _, ok := st.rules[r.Name] 358 | if ok == false { 359 | t.Errorf("AddRule for Actor [%s] should not fail", an) 360 | } 361 | } 362 | 363 | func TestStudioAddRules(t *testing.T) { 364 | st := NewStudio(2) 365 | r1 := &Rule{ 366 | Name: "rule1", 367 | Message: "message1", 368 | StrikeLimit: 3, 369 | ExpireBase: time.Millisecond * 10, 370 | Sentence: time.Millisecond * 10, 371 | } 372 | 373 | r2 := &Rule{ 374 | Name: "rule2", 375 | Message: "message2", 376 | StrikeLimit: 3, 377 | ExpireBase: time.Millisecond * 10, 378 | Sentence: time.Millisecond * 10, 379 | } 380 | 381 | // add rule safety is of no concern 382 | st.AddRule(r1) 383 | st.AddRule(r2) 384 | 385 | // apply rules 386 | err := st.CreateDirectors(1024) 387 | if err != nil { 388 | t.Errorf("CreateDirectors failed %v", err) 389 | } 390 | 391 | // range of rules and directors and make sure the rule exists for each 392 | for di, d := range st.directors { 393 | if !d.ruleExists(r1.Name) { 394 | t.Errorf("ApplyRules for director [%v] is missing rule %v", di, r1.Name) 395 | } 396 | if !d.ruleExists(r2.Name) { 397 | t.Errorf("ApplyRules for director [%v] is missing rule %v", di, r2.Name) 398 | } 399 | } 400 | 401 | } 402 | 403 | func TestStudioCreateDirectors(t *testing.T) { 404 | 405 | var nocap int32 406 | nocap = 256 407 | 408 | st := NewStudio(256) 409 | rn := "rn_" + strconv.FormatInt(time.Now().UnixNano(), 10) 410 | rm := "rm_" + strconv.FormatInt(time.Now().UnixNano(), 10) 411 | r := &Rule{ 412 | Name: rn, 413 | Message: rm, 414 | StrikeLimit: 3, 415 | ExpireBase: time.Millisecond * 10, 416 | Sentence: time.Millisecond * 10, 417 | } 418 | 419 | // add rule safety is of no concern 420 | st.AddRule(r) 421 | 422 | if st.capacity != nocap { 423 | t.Errorf("Capacity for Studio want [%v] got [%v]", st.capacity, nocap) 424 | } 425 | 426 | st.CreateDirectors(256) 427 | 428 | var i int32 429 | for i = 0; i < st.capacity; i++ { 430 | _, ok := st.directors[i] 431 | if !ok { 432 | t.Errorf("Director [%v] for Studio was not created", i) 433 | } 434 | } 435 | 436 | } 437 | --------------------------------------------------------------------------------