Skip to content

Commit

Permalink
Merge pull request #1449 from openziti/sdk-hosting-test
Browse files Browse the repository at this point in the history
sdk hosting test
  • Loading branch information
plorenz authored Oct 23, 2023
2 parents f41fcde + 9bed8a1 commit 76365e4
Show file tree
Hide file tree
Showing 117 changed files with 4,606 additions and 415 deletions.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
# Release 0.31.0

## What's New

* Rate limited for model changes

## Rate Limiter for Model Changes

To prevent the controller from being overwhelmed by a flood of changes, a rate limiter
can be enabled in the configuration file. A maximum number of queued changes can also
be configured. The rate limited is disabled by default for now. If not specified the
default number of queued changes is 100.

When the rate limit is hit, an error will be returned. If the request came in from
the REST API, the response will use HTTP status code 429 (too many requests).

The OpenAPI specs have been updated, so if you're using a generated client to make
REST calls, it's recommened that you regenerate your client.


```
commandRateLimiter:
enabled: true
maxQueued: 100
```

## Component Updates and Bug Fixes

* github.com/openziti/agent: [v1.0.15 -> v1.0.16](https://github.com/openziti/agent/compare/v1.0.15...v1.0.16)
* github.com/openziti/ziti: [v0.30.5 -> v0.30.6](https://github.com/openziti/ziti/compare/v0.30.5...v0.30.6)
* [Issue #1445](https://github.com/openziti/ziti/issues/1445) - Add controller update guardrail
* [Issue #1442](https://github.com/openziti/ziti/issues/1442) - Network watchdog not shutting down when controller shuts down


# Release 0.30.5

## What's New
Expand All @@ -11,6 +45,7 @@ Currently only HTTP Connect proxies which don't require authentication are suppo

**Example using `host.v1`**

```
{
"address": "192.168.2.50",
"port": 1234,
Expand All @@ -20,6 +55,7 @@ Currently only HTTP Connect proxies which don't require authentication are suppo
"type": "http"
}
}
```


## Component Updates and Bug Fixes
Expand Down
4 changes: 2 additions & 2 deletions controller/api_impl/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
openApiErrors "github.com/go-openapi/errors"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/foundation/v2/errorz"
"github.com/openziti/ziti/controller/api"
apierror2 "github.com/openziti/ziti/controller/apierror"
"github.com/openziti/ziti/controller/rest_model"
"github.com/openziti/foundation/v2/errorz"
"net/http"
)

Expand Down Expand Up @@ -124,7 +124,7 @@ func ToRestModel(e *errorz.ApiError, requestId string) *rest_model.APIError {
ret.Code = errorz.CouldNotValidateCode
ret.Message = errorz.CouldNotValidateMessage

} else if genericErr, ok := e.Cause.(apierror2.GenericCauseError); ok {
} else if genericErr, ok := e.Cause.(*apierror2.GenericCauseError); ok {
ret.Cause = &rest_model.APIErrorCause{
APIError: rest_model.APIError{
Data: genericErr.DataMap,
Expand Down
2 changes: 1 addition & 1 deletion controller/apierror/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type GenericCauseError struct {
DataMap map[string]interface{}
}

func (e GenericCauseError) Error() string {
func (e *GenericCauseError) Error() string {
return e.Message
}

Expand Down
8 changes: 8 additions & 0 deletions controller/apierror/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,11 @@ func NewEnrollmentExists(enrollmentMethod string) *errorz.ApiError {
AppendCause: true,
}
}

func NewTooManyUpdatesError() *errorz.ApiError {
return &errorz.ApiError{
Code: ServerTooManyRequestsCode,
Message: ServerTooManyRequestsMessage,
Status: ServerTooManyRequestsStatus,
}
}
4 changes: 4 additions & 0 deletions controller/apierror/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,8 @@ const (
EnrollmentExistsCode string = "ENROLLMENT_EXISTS"
EnrollmentExistsMessage string = "ENROLLMENT_EXISTS"
EnrollmentExistsStatus int = http.StatusConflict

ServerTooManyRequestsCode string = "SERVER_TOO_MANY_REQUESTS"
ServerTooManyRequestsMessage string = "Too many requests to alter state have been issued. Please slow your request rate or try again later."
ServerTooManyRequestsStatus int = http.StatusTooManyRequests
)
12 changes: 8 additions & 4 deletions controller/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ package command
import (
"github.com/michaelquigley/pfxlog"
"github.com/openziti/channel/v2"
"github.com/openziti/ziti/controller/change"
"github.com/openziti/foundation/v2/debugz"
"github.com/openziti/storage/boltz"
"github.com/openziti/ziti/controller/change"
"github.com/sirupsen/logrus"
"reflect"
)
Expand Down Expand Up @@ -56,6 +56,7 @@ type Dispatcher interface {
// LocalDispatcher should be used when running a non-clustered system
type LocalDispatcher struct {
EncodeDecodeCommands bool
Limiter RateLimiter
}

func (self *LocalDispatcher) IsLeaderOrLeaderless() bool {
Expand All @@ -82,7 +83,7 @@ func (self *LocalDispatcher) Dispatch(command Command) error {
if changeCtx == nil {
changeCtx = change.New().SetSourceType("unattributed").SetChangeAuthorType(change.AuthorTypeUnattributed)
}
ctx := changeCtx.NewMutateContext()

if self.EncodeDecodeCommands {
bytes, err := command.Encode()
if err != nil {
Expand All @@ -92,10 +93,13 @@ func (self *LocalDispatcher) Dispatch(command Command) error {
if err != nil {
return err
}
return cmd.Apply(ctx)
command = cmd
}

return command.Apply(ctx)
return self.Limiter.RunRateLimited(func() error {
ctx := changeCtx.NewMutateContext()
return command.Apply(ctx)
})
}

// Decoder instances know how to decode encoded commands
Expand Down
129 changes: 129 additions & 0 deletions controller/command/rate_limiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package command

import (
"github.com/openziti/metrics"
"github.com/openziti/ziti/controller/apierror"
"github.com/pkg/errors"
"sync/atomic"
"time"
)

const (
MetricLimiterCurrentQueuedCount = "command.limiter.queued_count"
MetricLimiterWorkTimer = "command.limiter.work_timer"

DefaultLimiterSize = 100
MinLimiterSize = 10
)

type RateLimiterConfig struct {
Enabled bool
QueueSize uint32
}

func NewRateLimiter(config RateLimiterConfig, registry metrics.Registry, closeNotify <-chan struct{}) RateLimiter {
if !config.Enabled {
return NoOpRateLimiter{}
}

if config.QueueSize < MinLimiterSize {
config.QueueSize = MinLimiterSize
}

result := &DefaultRateLimiter{
queue: make(chan *rateLimitedWork, config.QueueSize),
closeNotify: closeNotify,
workRate: registry.Timer(MetricLimiterWorkTimer),
}

if existing := registry.GetGauge(MetricLimiterCurrentQueuedCount); existing != nil {
existing.Dispose()
}

registry.FuncGauge(MetricLimiterCurrentQueuedCount, func() int64 {
return int64(result.currentSize.Load())
})

go result.run()

return result
}

type RateLimiter interface {
RunRateLimited(func() error) error
}

type NoOpRateLimiter struct{}

func (self NoOpRateLimiter) RunRateLimited(f func() error) error {
return f()
}

type rateLimitedWork struct {
wrapped func() error
result chan error
}

type DefaultRateLimiter struct {
currentSize atomic.Int32
queue chan *rateLimitedWork
closeNotify <-chan struct{}
workRate metrics.Timer
}

func (self *DefaultRateLimiter) RunRateLimited(f func() error) error {
work := &rateLimitedWork{
wrapped: f,
result: make(chan error, 1),
}
select {
case self.queue <- work:
self.currentSize.Add(1)
select {
case result := <-work.result:
return result
case <-self.closeNotify:
return errors.New("rate limiter shutting down")
}
case <-self.closeNotify:
return errors.New("rate limiter shutting down")
default:
return apierror.NewTooManyUpdatesError()
}
}

func (self *DefaultRateLimiter) run() {
defer self.workRate.Dispose()

for {
select {
case work := <-self.queue:
self.currentSize.Add(-1)
startTime := time.Now()
result := work.wrapped()
self.workRate.UpdateSince(startTime)
if result != nil {
work.result <- result
}
close(work.result)
case <-self.closeNotify:
return
}
}
}
36 changes: 32 additions & 4 deletions controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/channel/v2"
"github.com/openziti/identity"
"github.com/openziti/storage/boltz"
"github.com/openziti/transport/v2"
"github.com/openziti/ziti/common/config"
"github.com/openziti/ziti/common/pb/ctrl_pb"
"github.com/openziti/ziti/common/pb/mgmt_pb"
"github.com/openziti/ziti/controller/command"
"github.com/openziti/ziti/controller/db"
"github.com/openziti/ziti/controller/network"
"github.com/openziti/ziti/controller/raft"
"github.com/openziti/ziti/router/xgress"
"github.com/openziti/identity"
"github.com/openziti/storage/boltz"
"github.com/openziti/transport/v2"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"math"
"os"
"strings"
"time"
Expand Down Expand Up @@ -78,7 +80,8 @@ type Config struct {
InitialDelay time.Duration
}
}
src map[interface{}]interface{}
CommandRateLimiter command.RateLimiterConfig
src map[interface{}]interface{}
}

// CtrlOptions extends channel.Options to include support for additional, non-channel specific options
Expand Down Expand Up @@ -459,6 +462,31 @@ func LoadConfig(path string) (*Config, error) {
}
}

controllerConfig.CommandRateLimiter.QueueSize = command.DefaultLimiterSize

if value, found := cfgmap["commandRateLimiter"]; found {
if submap, ok := value.(map[interface{}]interface{}); ok {
if value, found := submap["enabled"]; found {
controllerConfig.CommandRateLimiter.Enabled = strings.EqualFold("true", fmt.Sprintf("%v", value))
}

if value, found := submap["maxQueued"]; found {
if intVal, ok := value.(int); ok {
v := int64(intVal)
if v < command.MinLimiterSize {
return nil, errors.Errorf("invalid value %v for commandRateLimiter, must be at least %v", value, command.MinLimiterSize)
}
if v > math.MaxUint32 {
return nil, errors.Errorf("invalid value %v for commandRateLimiter, must be at most %v", value, int64(math.MaxUint32))
}
controllerConfig.CommandRateLimiter.QueueSize = uint32(v)
} else {
return nil, errors.Errorf("invalid value %v for commandRateLimiter, must be integer value", value)
}
}
}
}

return controllerConfig, nil
}

Expand Down
Loading

0 comments on commit 76365e4

Please sign in to comment.