Skip to content

Commit

Permalink
[WIP] refactor(circuit breaker): move the circuit breaker/ retry logi…
Browse files Browse the repository at this point in the history
…c to client's round trip
  • Loading branch information
ispiroglu committed Nov 18, 2024
1 parent f30bb2a commit 5ce20e1
Show file tree
Hide file tree
Showing 22 changed files with 642 additions and 218 deletions.
58 changes: 58 additions & 0 deletions error/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package error

import (
"fmt"
"strings"
)

type ChakiError struct {
ClientName string
StatusCode int
RawBody []byte
ParsedBody interface{}
}

func (e *ChakiError) Error() string {
msg := fmt.Sprintf("Error on client %s (Status %d)", e.ClientName, e.StatusCode)
if details := e.extractErrorDetails(); details != "" {
msg += ": " + details
}

return msg
}

func (e *ChakiError) Status() int {
return e.StatusCode
}

type RandomError interface {
Status() int
}

func (e *ChakiError) extractErrorDetails() string {
var details []string

var extract func(interface{})
extract = func(v interface{}) {
switch value := v.(type) {
case string:
details = append(details, strings.TrimSpace(value))
case map[string]interface{}:
for _, v := range value {
extract(v)
}
case []interface{}:
for _, v := range value {
extract(v)
}
}
}

extract(e.ParsedBody)

if len(details) == 0 && len(e.RawBody) > 0 {
return strings.TrimSpace(string(e.RawBody))
}

return strings.Join(details, "; ")
}
80 changes: 80 additions & 0 deletions example/client-server-circuit/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"context"
"fmt"

"github.com/Trendyol/chaki/modules/client"
"github.com/Trendyol/chaki/modules/server/response"
)

type exampleClient struct {
*client.Base
}

func newClient(f *client.Factory) *exampleClient {
return &exampleClient{
Base: f.Get("example-client", client.WithDriverWrappers(HeaderWrapper())),
}
}

func (cl *exampleClient) SendHello(ctx context.Context) (string, error) {
resp := &response.Response[string]{}
if _, err := cl.Request(ctx).SetResult(resp).Get("/hello"); err != nil {
return "", err
}

return resp.Data, nil
}

func (cl *exampleClient) sendGreetWithQuery(ctx context.Context, req GreetWithQueryRequest) (string, error) {
resp := ""

params := map[string]string{
"text": req.Text,
"repeatTimes": fmt.Sprintf("%d", req.RepeatTimes),
}

r, err := cl.Request(ctx).
SetResult(resp).
SetQueryParams(params).
SetHeaders(params).
Get("/hello/query")
_ = r
if err != nil {
return "", err
}
return resp, nil
}

func (cl *exampleClient) sendGreetWithParam(ctx context.Context, req GreetWithParamRequest) (string, error) {
resp := &response.Response[string]{}

url := fmt.Sprintf("/hello/param/%s", req.Text)
params := map[string]string{
"repeatTimes": fmt.Sprintf("%d", req.RepeatTimes),
}

if _, err := cl.Request(ctx).
SetResult(resp).
SetHeaders(params).
SetQueryParams(params).
Get(url); err != nil {
return "", err
}

return resp.Data, nil
}

func (cl *exampleClient) sendGreetWithBody(ctx context.Context, req GreetWithBodyRequest) (string, error) {
resp := &response.Response[string]{}

if _, err := cl.Request(ctx).
SetResult(resp).
SetBody(req).
Post("/hello/body"); err != nil {
return "", err
}

return resp.Data, nil
}
30 changes: 30 additions & 0 deletions example/client-server-circuit/client/client_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"github.com/Trendyol/chaki/modules/client"
"github.com/go-resty/resty/v2"
)

type user struct {
publicUsername string
publicTag string
}

func HeaderWrapper() client.DriverWrapper {
return func(restyClient *resty.Client) *resty.Client {
return restyClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
ctx := r.Context()

h := map[string]string{}

if v := ctx.Value("user"); v != nil {
user := v.(user)
h["publicUsername"] = user.publicUsername
h["publicTag"] = user.publicTag
}

r.SetHeaders(h)
return nil
})
}
}
15 changes: 15 additions & 0 deletions example/client-server-circuit/client/error_decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"context"
"fmt"

"github.com/go-resty/resty/v2"
)

func customErrorDecoder(_ context.Context, res *resty.Response) error {
if res.StatusCode() == 404 {
return fmt.Errorf("not found")
}
return nil
}
75 changes: 75 additions & 0 deletions example/client-server-circuit/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"context"

"github.com/Trendyol/chaki"
"github.com/Trendyol/chaki/logger"
"github.com/Trendyol/chaki/modules/client"
"github.com/Trendyol/chaki/modules/server"
"github.com/Trendyol/chaki/modules/server/controller"
"github.com/Trendyol/chaki/modules/server/route"
"github.com/Trendyol/chaki/modules/swagger"
)

func main() {
app := chaki.New()

app.Use(
client.Module(),
server.Module(),

swagger.Module(),
)

app.Provide(
newClient,
NewController,
)

if err := app.Start(); err != nil {
logger.Fatal(err)
}
}

type serverController struct {
*controller.Base
cl *exampleClient
}

func NewController(cl *exampleClient) controller.Controller {
return &serverController{
Base: controller.New("server-controller").SetPrefix("/hello"),
cl: cl,
}
}

func (ct *serverController) hello(ctx context.Context, _ any) (string, error) {
logger.From(ctx).Info("hello")
resp, err := ct.cl.SendHello(ctx)
if err != nil {
return "", err
}
return resp, nil
}

func (ct *serverController) greetWithBody(ctx context.Context, req GreetWithBodyRequest) (string, error) {
return ct.cl.sendGreetWithBody(ctx, req)
}

func (ct *serverController) greetWithQuery(ctx context.Context, req GreetWithQueryRequest) (string, error) {
return ct.cl.sendGreetWithQuery(ctx, req)
}

func (ct *serverController) greetWithParam(ctx context.Context, req GreetWithParamRequest) (string, error) {
return ct.cl.sendGreetWithParam(ctx, req)
}

func (ct *serverController) Routes() []route.Route {
return []route.Route{
route.Get("", ct.hello),
route.Get("/query", ct.greetWithQuery).Name("Greet with query"),
route.Get("/param/:text", ct.greetWithParam).Name("Greet with param"),
route.Post("/body", ct.greetWithBody).Name("Greet with body"),
}
}
16 changes: 16 additions & 0 deletions example/client-server-circuit/client/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

type GreetWithBodyRequest struct {
Text string `json:"text" validate:"required,min=3,max=100"`
RepeatTimes int `json:"repeatTimes" validate:"required,gte=1,lte=5"`
}

type GreetWithQueryRequest struct {
Text string `query:"text" validate:"required,min=3,max=100"`
RepeatTimes int `query:"repeatTimes" validate:"required,gte=1,lte=5"`
}

type GreetWithParamRequest struct {
Text string `param:"text" validate:"required,min=3,max=100"`
RepeatTimes int `query:"repeatTimes" validate:"required,gte=1,lte=5"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
server:
addr: ":8081"

client:
retryPresets:
- name: "Custom Preset - 1"
count: 3
interval: 100ms
maxDelay: 2s
delayType: constant
example-client:
baseUrl: "http://localhost:8082"
circuit:
preset: custom
enabled: true
timeout: 10
maxConcurrentRequests: 1
errorPercentThreshold: 1
requestVolumeThreshold: 1
sleepWindow: 5000
retry:
enabled: true
preset: "Custom Preset - 1"







71 changes: 71 additions & 0 deletions example/client-server-circuit/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"context"
"errors"
"time"

"github.com/Trendyol/chaki"
"github.com/Trendyol/chaki/logger"
"github.com/Trendyol/chaki/modules/server"
"github.com/Trendyol/chaki/modules/server/controller"
"github.com/Trendyol/chaki/modules/server/route"
)

func main() {
app := chaki.New()

app.Use(
server.Module(),
)

app.Provide(
NewController,
)

if err := app.Start(); err != nil {
logger.Fatal(err)
}
}

type serverController struct {
*controller.Base
}

func NewController() controller.Controller {
return &serverController{
Base: controller.New("server-controller").SetPrefix("/hello"),
}
}

func (ct *serverController) hello(ctx context.Context, _ any) (string, error) {
logger.From(ctx).Info("hello from server")
return "Hi From Server", nil
}

func (ct *serverController) Routes() []route.Route {
return []route.Route{
route.Get("/", ct.hello),
route.Get("/greet", ct.greetHandler).Name("Greet Route"),
route.Get("/query", ct.greetWithQuery).Name("Greet with query"),
route.Get("/param/:text", ct.greetWithParam).Name("Greet with param"),
route.Post("/body", ct.greetWithBody).Name("Greet with body"),
}
}

func (ct *serverController) greetHandler(_ context.Context, _ struct{}) (string, error) {
return "", errors.New("server is initializing. please try again later")
}

func (ct *serverController) greetWithBody(_ context.Context, req GreetWithBodyRequest) (string, error) {
return req.Text, nil
}

func (ct *serverController) greetWithQuery(_ context.Context, req GreetWithQueryRequest) (string, error) {
time.Sleep(3 * time.Second)
return "", errors.New("server is initializing. please try again later")
}

func (ct *serverController) greetWithParam(_ context.Context, req GreetWithParamRequest) (string, error) {
return req.Text, nil
}
16 changes: 16 additions & 0 deletions example/client-server-circuit/server/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

type GreetWithBodyRequest struct {
Text string `json:"text" validate:"required,min=3,max=100"`
RepeatTimes int `json:"repeatTimes" validate:"required,gte=1,lte=5"`
}

type GreetWithQueryRequest struct {
Text string `query:"text" validate:"required,min=3,max=100"`
RepeatTimes int `query:"repeatTimes" validate:"required,gte=1,lte=5"`
}

type GreetWithParamRequest struct {
Text string `param:"text" validate:"required,min=3,max=100"`
RepeatTimes int `query:"repeatTimes" validate:"required,gte=1,lte=5"`
}
Loading

0 comments on commit 5ce20e1

Please sign in to comment.