diff --git a/activity/ratelimiter/README.md b/activity/ratelimiter/README.md index c3efd30..cebec17 100644 --- a/activity/ratelimiter/README.md +++ b/activity/ratelimiter/README.md @@ -1,12 +1,14 @@ # Rate Limiter -The `ratelimiter` service type creates a rate limiter with specified `limit`. When it is used in the `step`, it applies `limit` against supplied `token`. +The `ratelimiter` service type creates a rate limiter with specified `limit`. When it is used in the `step`, it applies `limit` against supplied `token`. If a `spikeThreshold` is specified then traffic will be blocked with a probability (that has an exponential decay) when a traffic spike occurs above the `spikeThreshold` multiple. The available service `settings` are as follows: | Name | Type | Description | |:-----------|:--------|:--------------| | limit | string | Limit can be specifed in the format of "limit-period". Valid periods are 'S', 'M' & 'H' to represent Second, Minute & Hour. Example: "10-S" represents 10 request/second | +| spikeThreshold | decimal | Multiple above base traffic load which triggers the spike block logic. Spike blocking is disabled by default. | +| decayRate | decimal | Exponential decay rate for the spike blocking probability. Default .01 | The available `input` for the request are as follows: diff --git a/activity/ratelimiter/activity.go b/activity/ratelimiter/activity.go index 4e77b46..a0e21c3 100644 --- a/activity/ratelimiter/activity.go +++ b/activity/ratelimiter/activity.go @@ -2,6 +2,10 @@ package ratelimiter import ( "context" + "math" + "math/rand" + "sync" + "time" "github.com/project-flogo/core/activity" "github.com/project-flogo/core/data/metadata" @@ -9,6 +13,11 @@ import ( "github.com/ulule/limiter/drivers/store/memory" ) +const ( + // MemorySize is the size of the circular buffer holding the request times + MemorySize = 256 +) + var ( activityMetadata = activity.ToMetadata(&Settings{}, &Input{}, &Output{}) ) @@ -17,6 +26,16 @@ func init() { activity.Register(&Activity{}, New) } +// Context is a token context +type Context struct { + sync.Mutex + rand *rand.Rand + index, prev, size int + lastSpike int64 + filter, lastRatio float64 + memory [MemorySize]int64 +} + // Activity is a rate limiter service // Limit can be specified in the format "-" // @@ -31,8 +50,58 @@ func init() { // * 5 requests / hour : "5-H" type Activity struct { limiter *limiter.Limiter + + sync.RWMutex + context map[string]*Context + threshold, decay float64 +} + +func (a *Activity) filterRequests(token string) bool { + a.RLock() + context := a.context[token] + a.RUnlock() + if context == nil { + context = &Context{ + prev: MemorySize - 1, + rand: rand.New(rand.NewSource(1)), + } + a.Lock() + a.context[token] = context + a.Unlock() + } + + context.Lock() + defer context.Unlock() + time := time.Now().UnixNano() + previous := context.memory[context.prev] + context.memory[context.index] = time + context.index, context.prev = (context.index+1)%MemorySize, context.index + size, valid := context.size, true + if size < MemorySize { + size++ + context.size, valid = size, false + } + oldest := context.memory[context.index] + + alpha := float64(time-previous) / float64(time-oldest) + rate := float64(size) / float64(time-oldest) + context.filter = alpha*rate + (1-alpha)*context.filter + ratio := rate / context.filter + if valid { + if ratio > a.threshold { + context.lastSpike, context.lastRatio = time, ratio-1 + } + + probability := 1 / (1 + context.lastRatio*math.Exp(a.decay*float64(context.lastSpike-time))) + if context.rand.Float64() > probability { + return true + } + } + + return false } +// New creates a new rate limiter func New(ctx activity.InitContext) (activity.Activity, error) { settings := Settings{} err := metadata.MapToStruct(ctx.Settings(), &settings, true) @@ -50,8 +119,15 @@ func New(ctx activity.InitContext) (activity.Activity, error) { store := memory.NewStore() limiter := limiter.New(store, rate) + if settings.DecayRate == 0 { + settings.DecayRate = .01 + } + act := Activity{ - limiter: limiter, + limiter: limiter, + context: make(map[string]*Context, 256), + threshold: settings.SpikeThreshold, + decay: settings.DecayRate, } return &act, nil @@ -90,9 +166,14 @@ func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { return true, nil } + filter := false + if a.threshold != 0 { + filter = a.filterRequests(input.Token) + } + // check the ratelimit output.LimitAvailable = limiterContext.Remaining - if limiterContext.Reached { + if limiterContext.Reached || filter { output.LimitReached = true } else { output.LimitReached = false diff --git a/activity/ratelimiter/activity_test.go b/activity/ratelimiter/activity_test.go index e42e3a2..b9a294f 100644 --- a/activity/ratelimiter/activity_test.go +++ b/activity/ratelimiter/activity_test.go @@ -142,3 +142,54 @@ func TestRatelimiter(t *testing.T) { assert.Nil(t, err) assert.False(t, ctx.output["limitReached"].(bool), "limit should not be reached") } + +func TestSmartRatelimiter(t *testing.T) { + activity, err := New(newInitContext(map[string]interface{}{ + "limit": "1000-S", + "spikeThreshold": "2", + })) + assert.Nil(t, err) + + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + ctx := newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + assert.False(t, ctx.output["limitReached"].(bool), "limit should not be reached") + } + blocked, notBlocked := 0, 0 + for i := 0; i < 10; i++ { + time.Sleep(10 * time.Millisecond) + ctx := newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + if ctx.output["limitReached"].(bool) { + blocked++ + } else { + notBlocked++ + } + } + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + ctx := newActivityContext(map[string]interface{}{ + "token": "abc123", + }) + _, err = activity.Eval(ctx) + assert.Nil(t, err) + if ctx.output["limitReached"].(bool) { + blocked++ + } else { + notBlocked++ + } + } + assert.Condition(t, func() (success bool) { + return blocked > 0 + }, "some requests should have been blocked") + assert.Condition(t, func() (success bool) { + return notBlocked > 0 + }, "some requests should not have been blocked") +} diff --git a/activity/ratelimiter/examples/activity_example.go b/activity/ratelimiter/examples/activity_example.go index db88443..1bee2d2 100644 --- a/activity/ratelimiter/examples/activity_example.go +++ b/activity/ratelimiter/examples/activity_example.go @@ -11,7 +11,7 @@ import ( ) // Example returns an API example -func Example(limit string) (engine.Engine, error) { +func Example(limit string, threshold float64) (engine.Engine, error) { app := api.NewApp() gateway := microapi.New("Pets") @@ -19,6 +19,7 @@ func Example(limit string) (engine.Engine, error) { serviceLimiter := gateway.NewService("RateLimiter", &ratelimiter.Activity{}) serviceLimiter.SetDescription("Rate limiter") serviceLimiter.AddSetting("limit", limit) + serviceLimiter.AddSetting("spikeThreshold", threshold) serviceStore := gateway.NewService("PetStorePets", &rest.Activity{}) serviceStore.SetDescription("Get pets by ID from the petstore") diff --git a/activity/ratelimiter/examples/api/README.md b/activity/ratelimiter/examples/api/basic/README.md similarity index 97% rename from activity/ratelimiter/examples/api/README.md rename to activity/ratelimiter/examples/api/basic/README.md index 2346558..6306859 100644 --- a/activity/ratelimiter/examples/api/README.md +++ b/activity/ratelimiter/examples/api/basic/README.md @@ -7,7 +7,7 @@ This recipe is a gateway which applies rate limit on specified dispatches. ## Setup ``` git clone https://github.com/project-flogo/microgateway -cd microgateway/activity/ratelimiter/examples/api +cd microgateway/activity/ratelimiter/examples/api/basic ``` ## Testing diff --git a/activity/ratelimiter/examples/api/main.go b/activity/ratelimiter/examples/api/basic/main.go similarity index 84% rename from activity/ratelimiter/examples/api/main.go rename to activity/ratelimiter/examples/api/basic/main.go index 1f5dc0f..f54f0c1 100644 --- a/activity/ratelimiter/examples/api/main.go +++ b/activity/ratelimiter/examples/api/basic/main.go @@ -6,7 +6,7 @@ import ( ) func main() { - e, err := examples.Example("3-M") + e, err := examples.Example("3-M", 0) if err != nil { panic(err) } diff --git a/activity/ratelimiter/examples/api/smart/README.md b/activity/ratelimiter/examples/api/smart/README.md new file mode 100644 index 0000000..45baa00 --- /dev/null +++ b/activity/ratelimiter/examples/api/smart/README.md @@ -0,0 +1,45 @@ +# Gateway with smart Rate Limiter +This recipe is a gateway which applies rate limit and traffic spike blocking on specified dispatches. + +## Installation +* Install [Go](https://golang.org/) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/ratelimiter/examples/api/smart +``` + +## Testing + +Start the gateway: +``` +go run main.go +``` + +### Run the client + +Run the following command: +``` +go run main.go -client +``` + +You should see the following like output: +``` +0 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} + +1 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} + +2 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} +``` + +After 256 requests there will be a spike and traffic, and then requests will be blocked: +``` +256 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} + +257 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} + +258 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} +``` + +After some time the requests will no longer be blocked. diff --git a/activity/ratelimiter/examples/api/smart/main.go b/activity/ratelimiter/examples/api/smart/main.go new file mode 100644 index 0000000..0719204 --- /dev/null +++ b/activity/ratelimiter/examples/api/smart/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "net/http" + "sync" + "time" + + "github.com/project-flogo/core/engine" + "github.com/project-flogo/microgateway/activity/ratelimiter/examples" +) + +var client = flag.Bool("client", false, "run the client") + +func main() { + flag.Parse() + + if *client { + client := &http.Client{} + request := func() []byte { + req, err := http.NewRequest(http.MethodGet, "http://localhost:9096/pets/1", nil) + if err != nil { + panic(err) + } + req.Header.Add("Token", "ABC123") + response, err := client.Do(req) + if err != nil { + panic(err) + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + response.Body.Close() + + return body + } + count := 0 + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + output := request() + fmt.Println(count, string(output)) + count++ + } + wait := sync.WaitGroup{} + wait.Add(10) + for i := 0; i < 10; i++ { + time.Sleep(10 * time.Millisecond) + go func(count int) { + output := request() + fmt.Println(count, string(output)) + wait.Done() + }(count) + count++ + } + wait.Wait() + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + output := request() + fmt.Println(count, string(output)) + count++ + } + return + } + + e, err := examples.Example("1000-S", 2) + if err != nil { + panic(err) + } + engine.RunEngine(e) +} diff --git a/activity/ratelimiter/examples/examples_test.go b/activity/ratelimiter/examples/examples_test.go index f7e1de5..95c0962 100644 --- a/activity/ratelimiter/examples/examples_test.go +++ b/activity/ratelimiter/examples/examples_test.go @@ -6,6 +6,8 @@ import ( "net/http" "path/filepath" "strconv" + "sync" + "sync/atomic" "testing" "time" @@ -93,7 +95,7 @@ func TestIntegrationAPI(t *testing.T) { {"3-M"}, {"1-S"}, } for i := range parameters { - e, err := Example(parameters[i].limit) + e, err := Example(parameters[i].limit, 0) assert.Nil(t, err) testApplication(t, e, parameters[i].limit) } @@ -108,11 +110,12 @@ func TestIntegrationJSON(t *testing.T) { }{ {"3-M"}, {"1-S"}, } - data, err := ioutil.ReadFile(filepath.FromSlash("./json/flogo.json")) + data, err := ioutil.ReadFile(filepath.FromSlash("./json/basic/flogo.json")) assert.Nil(t, err) for i := range parameters { var Input input err = json.Unmarshal(data, &Input) + assert.Nil(t, err) Input.Resources[0].Data.Services[0]["settings"] = map[string]interface{}{"limit": parameters[i].limit} data, _ = json.Marshal(Input) @@ -124,6 +127,102 @@ func TestIntegrationJSON(t *testing.T) { } } +func testSmartApplication(t *testing.T, e engine.Engine) { + const failCondition = "Rate Limit Exceeded - The service you have requested is over the allowed limit." + defer api.ClearResources() + test.Drain("9096") + err := e.Start() + assert.Nil(t, err) + defer func() { + err := e.Stop() + assert.Nil(t, err) + }() + test.Pour("9096") + + transport := &http.Transport{ + MaxIdleConns: 1, + } + defer transport.CloseIdleConnections() + client := &http.Client{ + Transport: transport, + } + + request := func(token string) Response { + req, err := http.NewRequest(http.MethodGet, "http://localhost:9096/pets/1", nil) + assert.Nil(t, err) + if token != "" { + req.Header.Add("Token", token) + } + response, err := client.Do(req) + assert.Nil(t, err) + body, err := ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + var rsp Response + err = json.Unmarshal(body, &rsp) + assert.Nil(t, err) + return rsp + } + + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + response := request("TEST") + assert.NotEqual(t, response.Status, failCondition) + } + wait, blocked, notBlocked := sync.WaitGroup{}, uint64(0), uint64(0) + wait.Add(10) + for i := 0; i < 10; i++ { + time.Sleep(10 * time.Millisecond) + go func() { + response := request("TEST") + if response.Status == failCondition { + atomic.AddUint64(&blocked, 1) + } else { + atomic.AddUint64(¬Blocked, 1) + } + wait.Done() + }() + } + wait.Wait() + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + response := request("TEST") + if response.Status == failCondition { + blocked++ + } else { + notBlocked++ + } + } + assert.Condition(t, func() (success bool) { + return blocked > 0 + }, "some requests should have been blocked") + assert.Condition(t, func() (success bool) { + return notBlocked > 0 + }, "some requests should not have been blocked") +} + +func TestIntegrationSmartAPI(t *testing.T) { + if testing.Short() { + t.Skip("skipping API integration test in short mode") + } + e, err := Example("1000-S", 2) + assert.Nil(t, err) + testSmartApplication(t, e) +} + +func TestIntegrationSmartJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping JSON integration test in short mode") + } + data, err := ioutil.ReadFile(filepath.FromSlash("./json/smart/flogo.json")) + assert.Nil(t, err) + cfg, err := engine.LoadAppConfig(string(data), false) + assert.Nil(t, err) + e, err := engine.New(cfg) + assert.Nil(t, err) + testSmartApplication(t, e) +} + //--------data structure-------// type input struct { @@ -135,7 +234,7 @@ type input struct { Channels interface{} `json:"channels"` Trig interface{} `json:"triggers"` Resources []struct { - Id string `json:"id"` + ID string `json:"id"` Compress bool `json:"compressed"` Data struct { Name string `json:"name"` diff --git a/activity/ratelimiter/examples/json/README.md b/activity/ratelimiter/examples/json/basic/README.md similarity index 97% rename from activity/ratelimiter/examples/json/README.md rename to activity/ratelimiter/examples/json/basic/README.md index 19b01ea..33b5750 100644 --- a/activity/ratelimiter/examples/json/README.md +++ b/activity/ratelimiter/examples/json/basic/README.md @@ -8,7 +8,7 @@ This recipe is a gateway which applies rate limit on specified dispatches. ## Setup ``` git clone https://github.com/project-flogo/microgateway -cd microgateway/activity/ratelimiter/examples/json +cd microgateway/activity/ratelimiter/examples/json/basic ``` ## Testing diff --git a/activity/ratelimiter/examples/json/flogo.json b/activity/ratelimiter/examples/json/basic/flogo.json similarity index 100% rename from activity/ratelimiter/examples/json/flogo.json rename to activity/ratelimiter/examples/json/basic/flogo.json diff --git a/activity/ratelimiter/examples/json/smart/README.md b/activity/ratelimiter/examples/json/smart/README.md new file mode 100644 index 0000000..f8a0283 --- /dev/null +++ b/activity/ratelimiter/examples/json/smart/README.md @@ -0,0 +1,52 @@ +# Gateway with smart Rate Limiter +This recipe is a gateway which applies rate limit and traffic spike blocking on specified dispatches. + +## Installation +* Install [Go](https://golang.org/) +* Install the flogo [cli](https://github.com/project-flogo/cli) + +## Setup +``` +git clone https://github.com/project-flogo/microgateway +cd microgateway/activity/ratelimiter/examples/json/smart +``` + +## Testing +Create the gateway: +``` +flogo create -f flogo.json +cd MyProxy +flogo build +``` + +Start the gateway: +``` +bin/MyProxy +``` + +### Run the client + +Run the following command: +``` +go run main.go -client +``` + +You should see the following like output: +``` +0 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} + +1 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} + +2 {"category":{"id":0,"name":"string"},"id":1,"name":"doggie","photoUrls":["string"],"status":"available","tags":[{"id":0,"name":"string"}]} +``` + +After 256 requests there will be a spike and traffic, and then requests will be blocked: +``` +256 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} + +257 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} + +258 {"status":"Rate Limit Exceeded - The service you have requested is over the allowed limit."} +``` + +After some time the requests will no longer be blocked. diff --git a/activity/ratelimiter/examples/json/smart/flogo.json b/activity/ratelimiter/examples/json/smart/flogo.json new file mode 100644 index 0000000..f2a1b2f --- /dev/null +++ b/activity/ratelimiter/examples/json/smart/flogo.json @@ -0,0 +1,116 @@ +{ + "name": "MyProxy", + "type": "flogo:app", + "version": "1.0.0", + "description": "Rate Limiter Gateway", + "properties": null, + "channels": null, + "triggers": [ + { + "name": "flogo-rest", + "id": "MyProxy", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "9096" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/pets/:petId" + }, + "actions": [ + { + "id": "microgateway:Pets" + } + ] + } + ] + } + ], + "resources": [ + { + "id": "microgateway:Pets", + "compressed": false, + "data": { + "name": "Pets", + "steps": [ + { + "service": "RateLimiter", + "input": { + "token": "=$.payload.headers.Token" + } + }, + { + "service": "PetStorePets", + "input": { + "pathParams": "=$.payload.pathParams" + } + } + ], + "responses": [ + { + "if": "$.RateLimiter.outputs.error == true", + "error": true, + "output": { + "code": 403, + "data": { + "status": "=$.RateLimiter.outputs.errorMessage" + } + } + }, + { + "if": "$.RateLimiter.outputs.limitReached == true", + "error": true, + "output": { + "code": 403, + "data": { + "status": "Rate Limit Exceeded - The service you have requested is over the allowed limit." + } + } + }, + { + "error": false, + "output": { + "code": 200, + "data": "=$.PetStorePets.outputs.data" + } + } + ], + "services": [ + { + "name": "RateLimiter", + "description": "Rate limiter", + "ref": "github.com/project-flogo/microgateway/activity/ratelimiter", + "settings": { + "limit": "1000-S", + "spikeThreshold": "2" + } + }, + { + "name": "PetStorePets", + "description": "Get pets by ID from the petstore", + "ref": "github.com/project-flogo/contrib/activity/rest", + "settings": { + "uri": "http://petstore.swagger.io/v2/pet/:petId", + "method": "GET", + "headers": { + "Accept": "application/json" + } + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/microgateway", + "settings": { + "uri": "microgateway:Pets" + }, + "id": "microgateway:Pets", + "metadata": null + } + ] +} diff --git a/activity/ratelimiter/examples/json/smart/main.go b/activity/ratelimiter/examples/json/smart/main.go new file mode 100644 index 0000000..b67580e --- /dev/null +++ b/activity/ratelimiter/examples/json/smart/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "net/http" + "sync" + "time" +) + +var client = flag.Bool("client", false, "run the client") + +func main() { + flag.Parse() + + if *client { + client := &http.Client{} + request := func() []byte { + req, err := http.NewRequest(http.MethodGet, "http://localhost:9096/pets/1", nil) + if err != nil { + panic(err) + } + req.Header.Add("Token", "ABC123") + response, err := client.Do(req) + if err != nil { + panic(err) + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + response.Body.Close() + + return body + } + count := 0 + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + output := request() + fmt.Println(count, string(output)) + count++ + } + wait := sync.WaitGroup{} + wait.Add(10) + for i := 0; i < 10; i++ { + time.Sleep(10 * time.Millisecond) + go func(count int) { + output := request() + fmt.Println(count, string(output)) + wait.Done() + }(count) + count++ + } + wait.Wait() + for i := 0; i < 256; i++ { + time.Sleep(50 * time.Millisecond) + output := request() + fmt.Println(count, string(output)) + count++ + } + return + } +} diff --git a/activity/ratelimiter/metadata.go b/activity/ratelimiter/metadata.go index f9ed46d..2feeccf 100644 --- a/activity/ratelimiter/metadata.go +++ b/activity/ratelimiter/metadata.go @@ -4,14 +4,19 @@ import ( "github.com/project-flogo/core/data/coerce" ) +// Settings are the settings for the rate limiter type Settings struct { - Limit string `md:"limit,required"` + Limit string `md:"limit,required"` + SpikeThreshold float64 `md:"spikeThreshold"` + DecayRate float64 `md:"decayRate"` } +// Input is the input for the rate limiter type Input struct { Token string `md:"token,required"` } +// FromMap converts the settings from a map of settings func (r *Input) FromMap(values map[string]interface{}) error { token, err := coerce.ToString(values["token"]) if err != nil { @@ -21,12 +26,14 @@ func (r *Input) FromMap(values map[string]interface{}) error { return nil } +// ToMap converts the settings to a map from a struct func (r *Input) ToMap() map[string]interface{} { return map[string]interface{}{ "token": r.Token, } } +// Output is the output of the rate limiter type Output struct { LimitReached bool `md:"limitReached"` LimitAvailable int64 `md:"limitAvailable"` @@ -34,6 +41,7 @@ type Output struct { ErrorMessage string `md:"errorMessage"` } +// FromMap converts the output from a map to a struct func (o *Output) FromMap(values map[string]interface{}) error { limitReached, err := coerce.ToBool(values["limitReached"]) if err != nil { @@ -58,6 +66,7 @@ func (o *Output) FromMap(values map[string]interface{}) error { return nil } +// ToMap converts the output to a map from a struct func (o *Output) ToMap() map[string]interface{} { return map[string]interface{}{ "limitReached": o.LimitReached,