From 997ba80dda60a86cb93ee2c837afc5ffe3cf9c22 Mon Sep 17 00:00:00 2001 From: Flynn Date: Thu, 3 Oct 2024 23:45:26 -0400 Subject: [PATCH 1/4] Prep for separating the multifunction workload from the color workload (since color is about to be gRPC) Signed-off-by: Flynn --- .goreleaser.yaml | 32 ++++++++++++++++++------------ cmd/generic/{ => workload}/main.go | 0 2 files changed, 19 insertions(+), 13 deletions(-) rename cmd/generic/{ => workload}/main.go (100%) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f235b3a..27d6138 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,7 +19,7 @@ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj -version: 1 +version: 2 # Allow overriding the registry and image name with environment variables. env: @@ -35,9 +35,9 @@ before: - make VERSION={{ .Version }} chart builds: - # "generic" is our build for any random hardware out there. - - id: generic - main: ./cmd/generic + # "generic-" builds are for any random hardware out there. + - id: generic-workload + main: ./cmd/generic/workload binary: workload env: - CGO_ENABLED=0 @@ -62,12 +62,13 @@ builds: # We build GUI and workload images for both arm64 and amd64, then build a # multiarch manifest from them. dockers: + ### GUI images - use: buildx goos: linux goarch: arm64 dockerfile: Dockerfiles/Dockerfile.gui ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-gui:{{ .Version }}-arm64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-gui:latest-arm64" @@ -80,7 +81,7 @@ dockers: goarch: amd64 dockerfile: Dockerfiles/Dockerfile.gui ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-gui:{{ .Version }}-amd64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-gui:latest-amd64" @@ -89,12 +90,13 @@ dockers: extra_files: - assets/html + ### Generic workload images: this is the multifunction image that can be any HTTP workload. - use: buildx goos: linux goarch: arm64 dockerfile: Dockerfiles/Dockerfile.workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:{{ .Version }}-arm64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:latest-arm64" @@ -105,7 +107,7 @@ dockers: goarch: amd64 dockerfile: Dockerfiles/Dockerfile.workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:{{ .Version }}-amd64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:latest-amd64" @@ -117,49 +119,53 @@ dockers: goarch: arm64 dockerfile: Dockerfiles/Dockerfile.external-workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-external-workload:{{ .Version }}-arm64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-external-workload:latest-arm64" build_flag_templates: - "--platform=linux/arm64" - "--build-arg=EXTERNAL_BASE={{ .Env.EXTERNAL_BASE }}" + - "--build-arg=WORKLOAD=workload" - use: buildx goos: linux goarch: amd64 dockerfile: Dockerfiles/Dockerfile.external-workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-external-workload:{{ .Version }}-amd64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-external-workload:latest-amd64" build_flag_templates: - "--platform=linux/amd64" - "--build-arg=EXTERNAL_BASE={{ .Env.EXTERNAL_BASE }}" + - "--build-arg=WORKLOAD=workload" - use: buildx goos: linux goarch: arm64 dockerfile: Dockerfiles/Dockerfile.bel-external-workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-bel-external-workload:{{ .Version }}-arm64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-bel-external-workload:latest-arm64" build_flag_templates: - "--platform=linux/arm64" - "--build-arg=EXTERNAL_BASE={{ .Env.BEL_EXTERNAL_BASE }}" + - "--build-arg=WORKLOAD=workload" - use: buildx goos: linux goarch: amd64 dockerfile: Dockerfiles/Dockerfile.bel-external-workload ids: - - generic + - generic-workload image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-bel-external-workload:{{ .Version }}-amd64" - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-bel-external-workload:latest-amd64" build_flag_templates: - "--platform=linux/amd64" - "--build-arg=EXTERNAL_BASE={{ .Env.BEL_EXTERNAL_BASE }}" + - "--build-arg=WORKLOAD=workload" # For the Pi, we only build an external-workload image for arm64. The Pi itself # is arm64, so there's no point in building for amd64, and we're not going to try @@ -276,7 +282,7 @@ archives: - id: generic name_template: '{{ .ProjectName }}_generic_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' builds: - - generic + - generic-workload - id: pi name_template: '{{ .ProjectName }}_pi_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' builds: diff --git a/cmd/generic/main.go b/cmd/generic/workload/main.go similarity index 100% rename from cmd/generic/main.go rename to cmd/generic/workload/main.go From 06261e3b3c64fb5db3a7b6b621b6bfb95127d1db Mon Sep 17 00:00:00 2001 From: Flynn Date: Thu, 3 Oct 2024 23:47:41 -0400 Subject: [PATCH 2/4] [WIP] Have a gRPC-based color workload, and teach the face workload how to call it. Needs some refactoring: lots of duplication between base_provider.go and baseserver.go Signed-off-by: Flynn --- Makefile | 12 +- README.md | 2 + cmd/generic/color/color.go | 121 ++++++++++++++++++ go.mod | 17 ++- go.sum | 22 +++- pkg/faces/base_provider.go | 224 +++++++++++++++++++++++++++++++++ pkg/faces/color.pb.go | 244 ++++++++++++++++++++++++++++++++++++ pkg/faces/color.proto | 21 ++++ pkg/faces/color_grpc.pb.go | 159 +++++++++++++++++++++++ pkg/faces/color_provider.go | 83 ++++++++++++ pkg/faces/faceserver.go | 110 +++++++++++++++- 11 files changed, 1007 insertions(+), 8 deletions(-) create mode 100644 cmd/generic/color/color.go create mode 100644 pkg/faces/base_provider.go create mode 100644 pkg/faces/color.pb.go create mode 100644 pkg/faces/color.proto create mode 100644 pkg/faces/color_grpc.pb.go create mode 100644 pkg/faces/color_provider.go diff --git a/Makefile b/Makefile index 470b1b5..e421da3 100644 --- a/Makefile +++ b/Makefile @@ -32,13 +32,21 @@ help: @echo "to the given HELM_REGISTRY. You must set both HELM_REGISTRY and VERSION" @echo "in order to use this target." @echo "" - @echo "'make deploy' will build and apply the k8s YAML into the faces" - @echo "namespace. This should be safe to do repeatedly." + @echo "'make proto" will regenerate Go code from protobuf definitions for" + @echo "the color workload. Requires protoc-gen-go to be installed." @echo "" @echo "You can also 'make clean' to remove all the Docker-image stuff," @echo "or 'make clobber' to smite everything and completely start over." .PHONY: help +proto: pkg/faces/color_grpc.pb.go pkg/faces/color.pb.go + +pkg/faces/color_grpc.pb.go pkg/faces/color.pb.go: pkg/faces/color.proto + protoc \ + --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + pkg/faces/color.proto + images: goreleaser release --snapshot --clean diff --git a/README.md b/README.md index 6ceb2d4..69c417b 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ The Faces architecture is fairly simple: then composes the responses together and returns the smiley/color combination to the GUI for display. + `face` uses HTTP to talk to `smiley` and gRPC to talk to `color`. + - The `smiley` workload returns a smiley face. By default, this is a grinning smiley, U+1F603, but you can set the `SMILEY` environment variable to any key in the `Smileys` map from `constants.go` to get a different smiley. diff --git a/cmd/generic/color/color.go b/cmd/generic/color/color.go new file mode 100644 index 0000000..c471776 --- /dev/null +++ b/cmd/generic/color/color.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2024 Buoyant Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2022-2024 Buoyant 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 +// +// http://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 main + +import ( + "context" + "log/slog" + "net" + "net/http" + "os" + + "flag" + "fmt" + + "github.com/BuoyantIO/faces-demo/v2/pkg/faces" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +type colorServer struct { + faces.UnimplementedColorServiceServer + provider *faces.ColorProvider +} + +func (srv *colorServer) Center(ctx context.Context, req *faces.ColorRequest) (*faces.ColorResponse, error) { + baseResp := srv.provider.Get(int(req.Row), int(req.Column)) + + slog.Debug(fmt.Sprintf("CENTER: %d, %d => %d, %s\n", req.Row, req.Column, baseResp.StatusCode, baseResp.Body)) + + switch baseResp.StatusCode { + case http.StatusOK: + color := baseResp.Body + + return &faces.ColorResponse{ + Color: color, + }, nil + + case http.StatusTooManyRequests: + return nil, status.Errorf(codes.ResourceExhausted, "rate limited: %s", baseResp.Body) + + default: + return nil, status.Errorf(codes.Internal, "failed to get color: %s", baseResp.Body) + } +} + +func (srv *colorServer) Edge(ctx context.Context, req *faces.ColorRequest) (*faces.ColorResponse, error) { + baseResp := srv.provider.Get(int(req.Row), int(req.Column)) + + slog.Debug(fmt.Sprintf("EDGE: %d, %d => %d, %s\n", req.Row, req.Column, baseResp.StatusCode, baseResp.Body)) + + switch baseResp.StatusCode { + case http.StatusOK: + color := baseResp.Body + + return &faces.ColorResponse{ + Color: color, + }, nil + + case http.StatusTooManyRequests: + return nil, status.Errorf(codes.ResourceExhausted, "rate limited: %s", baseResp.Body) + + default: + return nil, status.Errorf(codes.Internal, "failed to get color: %s", baseResp.Body) + } +} + +func main() { + logLevel := &slog.LevelVar{} // INFO + + slogOpts := &slog.HandlerOptions{ + Level: logLevel, + } + + logger := slog.New(slog.NewTextHandler(os.Stdout, slogOpts)) + slog.SetDefault(logger) + + logLevel.Set(slog.LevelDebug) + + // Define a command-line flag for the port number + port := flag.Int("port", 8000, "the port number to listen on") + flag.Parse() + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + + if err != nil { + slog.Error(fmt.Sprintf("failed to listen: %v", err)) + os.Exit(1) + } + + slog.Info(fmt.Sprintf("listening on %s", listener.Addr())) + var grpcOpts []grpc.ServerOption + + cprv := faces.NewColorProviderFromEnvironment() + server := &colorServer{provider: cprv} + + grpcServer := grpc.NewServer(grpcOpts...) + faces.RegisterColorServiceServer(grpcServer, server) + grpcServer.Serve(listener) +} diff --git a/go.mod b/go.mod index d9e6d43..16cc300 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,18 @@ module github.com/BuoyantIO/faces-demo/v2 -go 1.20 +go 1.21 -require github.com/warthog618/gpiod v0.8.2 +toolchain go1.23.1 -require golang.org/x/sys v0.17.0 // indirect +require ( + github.com/warthog618/gpiod v0.8.2 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.34.2 +) + +require ( + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect +) diff --git a/go.sum b/go.sum index 0fa8bce..fa78ed7 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,28 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/warthog618/go-gpiosim v0.1.0 h1:2rTMTcKUVZxpUuvRKsagnKAbKpd3Bwffp87xywEDVGI= +github.com/warthog618/go-gpiosim v0.1.0/go.mod h1:Ngx/LYI5toxHr4E+Vm6vTgCnt0of0tktsSuMUEJ2wCI= github.com/warthog618/gpiod v0.8.2 h1:2HgQ9pNowPp7W77sXhX5ut5Tqq1WoS3t7bXYDxtYvxc= github.com/warthog618/gpiod v0.8.2/go.mod h1:O7BNpHjCn/4YS5yFVmoFZAlY1LuYuQ8vhPf0iy/qdi4= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/faces/base_provider.go b/pkg/faces/base_provider.go new file mode 100644 index 0000000..29e7735 --- /dev/null +++ b/pkg/faces/base_provider.go @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2024 Buoyant Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2022-2024 Buoyant 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 +// +// http://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 faces + +import ( + "fmt" + "log/slog" + "math/rand" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/BuoyantIO/faces-demo/v2/pkg/utils" +) + +type ProviderResponse struct { + StatusCode int + Body string +} + +type BaseProvider struct { + lock sync.Mutex + logger *slog.Logger + delayBuckets []int + errorFraction int + latchFraction int + maxRate float64 + userHeaderName string + hostIP string + debugEnabled bool + + latched bool + rateCounter *utils.RateCounter + lastRequestTime time.Time +} + +func (bprv *BaseProvider) SetupFromEnvironment() { + delayBucketsStr := utils.StringFromEnv("DELAY_BUCKETS", "") + + if delayBucketsStr != "" { + delayBuckets := strings.Split(delayBucketsStr, ",") + for _, bucketStr := range delayBuckets { + bucket, err := strconv.Atoi(bucketStr) + if err == nil { + if bucket < 0 { + bucket = 0 + } + + bprv.delayBuckets = append(bprv.delayBuckets, bucket) + } + } + } + + bprv.errorFraction = utils.PercentageFromEnv("ERROR_FRACTION", 0) + bprv.latchFraction = utils.PercentageFromEnv("LATCH_FRACTION", 0) + + bprv.debugEnabled = utils.BoolFromEnv("DEBUG_ENABLED", false) + + bprv.maxRate = utils.FloatFromEnv("MAX_RATE", 0.0) + + if bprv.maxRate >= 0.1 { + bprv.rateCounter = utils.NewRateCounter(10) + } + + bprv.userHeaderName = utils.StringFromEnv("USER_HEADER_NAME", "X-Faces-User") + bprv.hostIP = utils.StringFromEnv("HOST_IP", utils.StringFromEnv("HOSTNAME", "unknown")) + + bprv.Infof("booted on %s", bprv.hostIP) + bprv.Infof("delay_buckets %v", bprv.delayBuckets) + bprv.Infof("error_fraction %d", bprv.errorFraction) + bprv.Infof("latch_fraction %d", bprv.latchFraction) + bprv.Infof("debug_enabled %v", bprv.debugEnabled) + bprv.Infof("max_rate %f", bprv.maxRate) + bprv.Infof("userHeaderName %v", bprv.userHeaderName) +} + +func (bprv *BaseProvider) Infof(format string, args ...interface{}) { + bprv.logger.Info(fmt.Sprintf(format, args...)) +} + +func (bprv *BaseProvider) Debugf(format string, args ...interface{}) { + bprv.logger.Debug(fmt.Sprintf(format, args...)) +} + +func (bprv *BaseProvider) Warnf(format string, args ...interface{}) { + bprv.logger.Warn(fmt.Sprintf(format, args...)) +} + +func (bprv *BaseProvider) SetLogger(logger *slog.Logger) { + bprv.logger = logger +} + +func (bprv *BaseProvider) SetDebug(debug bool) { + bprv.debugEnabled = debug +} + +func (bprv *BaseProvider) Lock() { + bprv.lock.Lock() +} + +func (bprv *BaseProvider) Unlock() { + bprv.lock.Unlock() +} + +func (bprv *BaseProvider) IsLatched() bool { + return bprv.latched +} + +func (bprv *BaseProvider) SetLatched(latched bool) { + bprv.latched = latched +} + +// CheckUnlatch checks to see if we should unlatch the provider. +func (bprv *BaseProvider) CheckUnlatch(now time.Time) { + bprv.lock.Lock() + defer bprv.lock.Unlock() + + if bprv.latched { + // How long has it been since our last request? + delta := now.Sub(bprv.lastRequestTime) + + if delta.Seconds() > 30 { + // It's been thirty full seconds since our last request. If we + // were latched into the error state, it's time to come out. + bprv.latched = false + } + } +} + +// DelayIfNeeded delays if there are delay buckets set. +func (bprv *BaseProvider) DelayIfNeeded() { + if len(bprv.delayBuckets) > 0 { + delayMs := bprv.delayBuckets[rand.Intn(len(bprv.delayBuckets))] + time.Sleep(time.Duration(delayMs) * time.Millisecond) + } +} + +// CheckRequestStatus checks the state of the provider and decides whether +// it's OK to have the request continue, or whether it should be failed for +// various reasons: +// +// - If maxRate is set, then we first check the rate counter to see if we +// need to fail due to rate limiting. +// - Otherwise, if we're latched into an error state, then we immediately fail. +// - Otherwise, if errorFraction is set, then errorFraction% of requests will fail +// and, if latchFraction is set, then every error has a latchFraction % chance +// to latch the error state. + +func (bprv *BaseProvider) CheckRequestStatus() *BaseRequestStatus { + // We need to figure out if we're going to send an error. + rstat := &BaseRequestStatus{ + // It's true that not every provider uses HTTP, but we're going + // to use the HTTP status codes as a common way to signal errors. + statusCode: http.StatusOK, + } + + start := time.Now() + + // Is a rate limiter active? We do this first because if the rate limiter + // trips, we want the service to be unable to do _any_ processing, including + // checking for other errors. + + if bprv.rateCounter != nil { + bprv.rateCounter.Mark(start) + rate := bprv.rateCounter.CurrentRate() + + if rate >= bprv.maxRate { + // Bzzzt! Rate limited. + rstat.ratelimited = true + rstat.message = fmt.Sprintf("Rate limited (%.1f RPS > max %.1f RPS)", rate, bprv.maxRate) + } + } + + // OK, if rate limiting didn't get us, we might still have an error. + if !rstat.ratelimited { + // If we've gotten latched into an error state, we're definitely sending + // an error. + + if bprv.IsLatched() { + rstat.latched = true + rstat.errored = true + rstat.message = "Latched into error state" + rstat.statusCode = 599 + } else if bprv.errorFraction > 0 { + // Not latched, but there's a chance of an error here too. + if rand.Intn(100) <= bprv.errorFraction { + bprv.Debugf("error fraction triggered") + + // Yup. Error. + rstat.errored = true + rstat.message = "" // No message, the provider will fill this in. + rstat.statusCode = 500 + + // We might get latched here, too. + if bprv.latchFraction > 0 && rand.Intn(100) <= bprv.latchFraction { + bprv.SetLatched(true) + + rstat.latched = true + rstat.message = "Latched into error state" + rstat.statusCode = 599 + } + } + } + } + + return rstat +} diff --git a/pkg/faces/color.pb.go b/pkg/faces/color.pb.go new file mode 100644 index 0000000..f992d3e --- /dev/null +++ b/pkg/faces/color.pb.go @@ -0,0 +1,244 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.20.3 +// source: pkg/faces/color.proto + +package faces + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ColorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Row int32 `protobuf:"varint,1,opt,name=row,proto3" json:"row,omitempty"` + Column int32 `protobuf:"varint,2,opt,name=column,proto3" json:"column,omitempty"` +} + +func (x *ColorRequest) Reset() { + *x = ColorRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_faces_color_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ColorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ColorRequest) ProtoMessage() {} + +func (x *ColorRequest) ProtoReflect() protoreflect.Message { + mi := &file_pkg_faces_color_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ColorRequest.ProtoReflect.Descriptor instead. +func (*ColorRequest) Descriptor() ([]byte, []int) { + return file_pkg_faces_color_proto_rawDescGZIP(), []int{0} +} + +func (x *ColorRequest) GetRow() int32 { + if x != nil { + return x.Row + } + return 0 +} + +func (x *ColorRequest) GetColumn() int32 { + if x != nil { + return x.Column + } + return 0 +} + +type ColorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Color string `protobuf:"bytes,1,opt,name=color,proto3" json:"color,omitempty"` + Rate string `protobuf:"bytes,2,opt,name=rate,proto3" json:"rate,omitempty"` + Errors []string `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` +} + +func (x *ColorResponse) Reset() { + *x = ColorResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_faces_color_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ColorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ColorResponse) ProtoMessage() {} + +func (x *ColorResponse) ProtoReflect() protoreflect.Message { + mi := &file_pkg_faces_color_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ColorResponse.ProtoReflect.Descriptor instead. +func (*ColorResponse) Descriptor() ([]byte, []int) { + return file_pkg_faces_color_proto_rawDescGZIP(), []int{1} +} + +func (x *ColorResponse) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + +func (x *ColorResponse) GetRate() string { + if x != nil { + return x.Rate + } + return "" +} + +func (x *ColorResponse) GetErrors() []string { + if x != nil { + return x.Errors + } + return nil +} + +var File_pkg_faces_color_proto protoreflect.FileDescriptor + +var file_pkg_faces_color_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x70, 0x6b, 0x67, 0x2f, 0x66, 0x61, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x6f, 0x6c, 0x6f, + 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x38, 0x0a, 0x0c, 0x43, 0x6f, 0x6c, 0x6f, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x6f, 0x77, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x72, 0x6f, 0x77, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, + 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x22, 0x51, 0x0a, 0x0d, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x32, 0x5e, 0x0a, 0x0c, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x0d, + 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, + 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, + 0x04, 0x45, 0x64, 0x67, 0x65, 0x12, 0x0d, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x42, 0x75, 0x6f, 0x79, 0x61, 0x6e, 0x74, 0x49, 0x4f, 0x2f, 0x66, 0x61, 0x63, + 0x65, 0x73, 0x2d, 0x64, 0x65, 0x6d, 0x6f, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x66, + 0x61, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_faces_color_proto_rawDescOnce sync.Once + file_pkg_faces_color_proto_rawDescData = file_pkg_faces_color_proto_rawDesc +) + +func file_pkg_faces_color_proto_rawDescGZIP() []byte { + file_pkg_faces_color_proto_rawDescOnce.Do(func() { + file_pkg_faces_color_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_faces_color_proto_rawDescData) + }) + return file_pkg_faces_color_proto_rawDescData +} + +var file_pkg_faces_color_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_faces_color_proto_goTypes = []any{ + (*ColorRequest)(nil), // 0: ColorRequest + (*ColorResponse)(nil), // 1: ColorResponse +} +var file_pkg_faces_color_proto_depIdxs = []int32{ + 0, // 0: ColorService.Center:input_type -> ColorRequest + 0, // 1: ColorService.Edge:input_type -> ColorRequest + 1, // 2: ColorService.Center:output_type -> ColorResponse + 1, // 3: ColorService.Edge:output_type -> ColorResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_pkg_faces_color_proto_init() } +func file_pkg_faces_color_proto_init() { + if File_pkg_faces_color_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_faces_color_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*ColorRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_faces_color_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*ColorResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_faces_color_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pkg_faces_color_proto_goTypes, + DependencyIndexes: file_pkg_faces_color_proto_depIdxs, + MessageInfos: file_pkg_faces_color_proto_msgTypes, + }.Build() + File_pkg_faces_color_proto = out.File + file_pkg_faces_color_proto_rawDesc = nil + file_pkg_faces_color_proto_goTypes = nil + file_pkg_faces_color_proto_depIdxs = nil +} diff --git a/pkg/faces/color.proto b/pkg/faces/color.proto new file mode 100644 index 0000000..6d98e54 --- /dev/null +++ b/pkg/faces/color.proto @@ -0,0 +1,21 @@ +// If you modify this file, you'll need to rerun 'make proto'! + +syntax = "proto3"; + +option go_package="github.com/BuoyantIO/faces-demo/v2/pkg/faces"; + +service ColorService { + rpc Center (ColorRequest) returns (ColorResponse); + rpc Edge (ColorRequest) returns (ColorResponse); +} + +message ColorRequest { + int32 row = 1; + int32 column = 2; +} + +message ColorResponse { + string color = 1; + string rate = 2; + repeated string errors = 3; +} diff --git a/pkg/faces/color_grpc.pb.go b/pkg/faces/color_grpc.pb.go new file mode 100644 index 0000000..ab1121e --- /dev/null +++ b/pkg/faces/color_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.20.3 +// source: pkg/faces/color.proto + +package faces + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ColorService_Center_FullMethodName = "/ColorService/Center" + ColorService_Edge_FullMethodName = "/ColorService/Edge" +) + +// ColorServiceClient is the client API for ColorService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ColorServiceClient interface { + Center(ctx context.Context, in *ColorRequest, opts ...grpc.CallOption) (*ColorResponse, error) + Edge(ctx context.Context, in *ColorRequest, opts ...grpc.CallOption) (*ColorResponse, error) +} + +type colorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewColorServiceClient(cc grpc.ClientConnInterface) ColorServiceClient { + return &colorServiceClient{cc} +} + +func (c *colorServiceClient) Center(ctx context.Context, in *ColorRequest, opts ...grpc.CallOption) (*ColorResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ColorResponse) + err := c.cc.Invoke(ctx, ColorService_Center_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *colorServiceClient) Edge(ctx context.Context, in *ColorRequest, opts ...grpc.CallOption) (*ColorResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ColorResponse) + err := c.cc.Invoke(ctx, ColorService_Edge_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ColorServiceServer is the server API for ColorService service. +// All implementations must embed UnimplementedColorServiceServer +// for forward compatibility. +type ColorServiceServer interface { + Center(context.Context, *ColorRequest) (*ColorResponse, error) + Edge(context.Context, *ColorRequest) (*ColorResponse, error) + mustEmbedUnimplementedColorServiceServer() +} + +// UnimplementedColorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedColorServiceServer struct{} + +func (UnimplementedColorServiceServer) Center(context.Context, *ColorRequest) (*ColorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Center not implemented") +} +func (UnimplementedColorServiceServer) Edge(context.Context, *ColorRequest) (*ColorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Edge not implemented") +} +func (UnimplementedColorServiceServer) mustEmbedUnimplementedColorServiceServer() {} +func (UnimplementedColorServiceServer) testEmbeddedByValue() {} + +// UnsafeColorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ColorServiceServer will +// result in compilation errors. +type UnsafeColorServiceServer interface { + mustEmbedUnimplementedColorServiceServer() +} + +func RegisterColorServiceServer(s grpc.ServiceRegistrar, srv ColorServiceServer) { + // If the following call pancis, it indicates UnimplementedColorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ColorService_ServiceDesc, srv) +} + +func _ColorService_Center_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ColorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ColorServiceServer).Center(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ColorService_Center_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ColorServiceServer).Center(ctx, req.(*ColorRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ColorService_Edge_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ColorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ColorServiceServer).Edge(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ColorService_Edge_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ColorServiceServer).Edge(ctx, req.(*ColorRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ColorService_ServiceDesc is the grpc.ServiceDesc for ColorService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ColorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ColorService", + HandlerType: (*ColorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Center", + Handler: _ColorService_Center_Handler, + }, + { + MethodName: "Edge", + Handler: _ColorService_Edge_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "pkg/faces/color.proto", +} diff --git a/pkg/faces/color_provider.go b/pkg/faces/color_provider.go new file mode 100644 index 0000000..da52ff7 --- /dev/null +++ b/pkg/faces/color_provider.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Buoyant Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2022-2024 Buoyant 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 +// +// http://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 faces + +import ( + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/BuoyantIO/faces-demo/v2/pkg/utils" +) + +type ColorProvider struct { + BaseProvider + color string +} + +func NewColorProviderFromEnvironment() *ColorProvider { + cprv := &ColorProvider{} + cprv.SetLogger(slog.Default().With( + "provider", "ColorProvider", + )) + + cprv.BaseProvider.SetupFromEnvironment() + + colorName := utils.StringFromEnv("COLOR", "blue") + cprv.color = Colors.Lookup(colorName) + + cprv.Infof("Using %s => %s\n", colorName, cprv.color) + return cprv +} + +func (cprv *ColorProvider) Get(row, col int) ProviderResponse { + start := time.Now() + + resp := ProviderResponse{ + StatusCode: http.StatusOK, + } + + cprv.CheckUnlatch(start) + rstat := cprv.CheckRequestStatus() + + if rstat.IsRateLimited() { + cprv.Warnf("RATELIMIT(%d, %d) => %s\n", row, col, rstat.Message()) + + resp.StatusCode = http.StatusTooManyRequests + resp.Body = rstat.Message() + } else if rstat.IsErrored() { + msg := rstat.Message() + + if msg == "" { + msg = fmt.Sprintf("Color error! (error fraction %d%%)", cprv.errorFraction) + } + + cprv.Warnf("ERROR(%d, %d) => %d, %s\n", row, col, rstat.StatusCode(), msg) + + resp.StatusCode = rstat.StatusCode() + resp.Body = msg + } else { + cprv.Infof("OK(%d, %d) => %d, %s\n", row, col, rstat.StatusCode(), cprv.color) + + resp.StatusCode = rstat.StatusCode() + resp.Body = cprv.color + } + + return resp +} diff --git a/pkg/faces/faceserver.go b/pkg/faces/faceserver.go index c04b5a4..23c2970 100644 --- a/pkg/faces/faceserver.go +++ b/pkg/faces/faceserver.go @@ -18,14 +18,19 @@ package faces import ( + context "context" "encoding/json" "fmt" "io" + "net" "net/http" + "strconv" "strings" "time" "github.com/BuoyantIO/faces-demo/v2/pkg/utils" + grpc "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) type FaceServer struct { @@ -60,6 +65,25 @@ func (srv *FaceServer) SetupFromEnvironment() { srv.smileyService = utils.StringFromEnv("SMILEY_SERVICE", "smiley") srv.colorService = utils.StringFromEnv("COLOR_SERVICE", "color") + _, _, err := net.SplitHostPort(srv.colorService) + + if err != nil { + // Most likely we're missing the port, so try to default it. + addr := net.ParseIP(srv.colorService) + + if addr != nil { + // Is this an IPv6 address? + if strings.Contains(srv.colorService, ":") { + srv.colorService = fmt.Sprintf("[%s]:80", srv.colorService) + } else { + srv.colorService = fmt.Sprintf("%s:80", srv.colorService) + } + } else { + // Probably a hostname. + srv.colorService = fmt.Sprintf("%s:80", srv.colorService) + } + } + fmt.Printf("%s %s: smileyService %v\n", time.Now().Format(time.RFC3339), srv.Name, srv.smileyService) fmt.Printf("%s %s: colorService %v\n", time.Now().Format(time.RFC3339), srv.Name, srv.colorService) } @@ -179,6 +203,8 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) StatusCode: http.StatusOK, } + fmt.Printf("%s %s: request path: %s, query string: %s\n", time.Now().Format(time.RFC3339), srv.Name, r.URL.Path, r.URL.RawQuery) + // Our request URL should start with /center/ or /edge/, and we want to // propagate that to our smiley and color services. subrequest := strings.Split(r.URL.Path, "/")[1] @@ -190,6 +216,33 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) rateStr := fmt.Sprintf("%.1f RPS", srv.CurrentRate()) + query := r.URL.Query() + query_row := query.Get("row") + query_column := query.Get("col") + + row := -1 + column := -1 + + if query_row != "" { + r, err := strconv.Atoi(query_row) + + if err == nil { + row = r + } else { + fmt.Printf("%s %s: couldn't parse row %s, using -1: %s\n", time.Now().Format(time.RFC3339), srv.Name, query_row, err) + } + } + + if query_column != "" { + c, err := strconv.Atoi(query_column) + + if err == nil { + column = c + } else { + fmt.Printf("%s %s: couldn't parse column %s, using -1: %s\n", time.Now().Format(time.RFC3339), srv.Name, query_column, err) + } + } + if rstat.IsRateLimited() { errors = append(errors, rstat.Message()) smiley, smileyOK = Smileys.Lookup(Defaults["smiley-ratelimit"]) @@ -216,7 +269,62 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) }() go func() { - colorCh <- srv.makeRequest(user, userAgent, srv.colorService, "color", subrequest) + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + + conn, err := grpc.NewClient(srv.colorService, opts...) + + if err != nil { + errors = append(errors, fmt.Sprintf("color: %s", err)) + colorCh <- &FaceResponse{ + statusCode: http.StatusInternalServerError, + data: fmt.Sprintf("couldn't connect to %s: %s", srv.colorService, err), + } + return + } + + defer conn.Close() + + client := NewColorServiceClient(conn) + colorReq := &ColorRequest{ + Row: int32(row), + Column: int32(column), + } + + var colorResp *ColorResponse + + if srv.debugEnabled { + fmt.Printf("%s %s: starting gRPC to %s for %s (row %d col %d)\n", + time.Now().Format(time.RFC3339), srv.Name, srv.colorService, subrequest, colorReq.Row, colorReq.Column) + } + + if subrequest == "center" { + colorResp, err = client.Center(context.Background(), colorReq) + } else { + colorResp, err = client.Edge(context.Background(), colorReq) + } + + if err != nil { + if srv.debugEnabled { + fmt.Printf("%s %s: gRPC failed: %s\n", time.Now().Format(time.RFC3339), srv.Name, err) + } + + errors = append(errors, fmt.Sprintf("color: %s", err)) + colorCh <- &FaceResponse{ + statusCode: http.StatusInternalServerError, + data: fmt.Sprintf("couldn't get color from %s: %s", srv.colorService, err), + } + } else { + if srv.debugEnabled { + fmt.Printf("%s %s: gRPC got %#v\n", time.Now().Format(time.RFC3339), srv.Name, colorResp) + } + + colorCh <- &FaceResponse{ + statusCode: http.StatusOK, + data: colorResp.Color, + } + } }() // Wait for the responses from both services From 7c923a02e9db5e0f3afdadc1849be1ab0d921356 Mon Sep 17 00:00:00 2001 From: Flynn Date: Thu, 3 Oct 2024 23:48:14 -0400 Subject: [PATCH 3/4] Teach goreleaser how to build color Signed-off-by: Flynn --- .goreleaser.yaml | 40 +++++++++++++++++++++++++++++++++ Dockerfiles/Dockerfile.workload | 3 ++- faces-chart/values.yaml | 4 ++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 27d6138..cfe643c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -47,6 +47,17 @@ builds: - arm64 - amd64 + - id: generic-color + main: ./cmd/generic/color + binary: color-workload + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - arm64 + - amd64 + # "pi" is our build specifically for the Raspberry Pi (it uses Pi GPIO for # some LEDs and a knob). - id: pi @@ -102,6 +113,7 @@ dockers: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:latest-arm64" build_flag_templates: - "--platform=linux/arm64" + - "--build-arg=WORKLOAD=workload" - use: buildx goos: linux goarch: amd64 @@ -113,6 +125,33 @@ dockers: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-workload:latest-amd64" build_flag_templates: - "--platform=linux/amd64" + - "--build-arg=WORKLOAD=workload" + + ### Color workload images: this can only be a gRPC color workload. + - use: buildx + goos: linux + goarch: arm64 + dockerfile: Dockerfiles/Dockerfile.workload + ids: + - generic-color + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-color:{{ .Version }}-arm64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-color:latest-arm64" + build_flag_templates: + - "--platform=linux/arm64" + - "--build-arg=WORKLOAD=color-workload" + - use: buildx + goos: linux + goarch: amd64 + dockerfile: Dockerfiles/Dockerfile.workload + ids: + - generic-color + image_templates: + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-color:{{ .Version }}-amd64" + - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}-color:latest-amd64" + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=WORKLOAD=color-workload" - use: buildx goos: linux @@ -283,6 +322,7 @@ archives: name_template: '{{ .ProjectName }}_generic_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' builds: - generic-workload + - generic-color - id: pi name_template: '{{ .ProjectName }}_pi_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' builds: diff --git a/Dockerfiles/Dockerfile.workload b/Dockerfiles/Dockerfile.workload index a57bae3..bf2089b 100644 --- a/Dockerfiles/Dockerfile.workload +++ b/Dockerfiles/Dockerfile.workload @@ -19,12 +19,13 @@ # Use a minimal base image for the final image FROM scratch AS final +ARG WORKLOAD # This is associated with the faces-demo repo. LABEL org.opencontainers.image.source=https://github.com/BuoyantIO/faces-demo # Copy the compiled binary from the builder stage into the final image -COPY workload /workload +COPY ${WORKLOAD} /workload # Set the entrypoint to the compiled binary ENTRYPOINT ["/workload"] diff --git a/faces-chart/values.yaml b/faces-chart/values.yaml index bb89a2f..91f4d62 100644 --- a/faces-chart/values.yaml +++ b/faces-chart/values.yaml @@ -65,7 +65,7 @@ smiley2: color: color: "" # Override if desired, defaults to colorblind-friendly light blue from the Tol palette # image: "" # If set, overrides the imageName/imageTag pair - # imageName: "" # If not set, uses backend.imageName + imageName: ghcr.io/buoyantio/faces-color # imageTag: "" # If not set, uses backend.imageTag # imagePullPolicy: "" # If not set, uses backend.imagePullPolicy # errorFraction: "" # If not set, uses backend.errorFraction @@ -75,7 +75,7 @@ color2: enabled: False # If set to True, enables the second color workload color: "green" # Override if desired, defaults to colorblind-friendly green from the Tol palette # image: "" # If set, overrides the imageName/imageTag pair - # imageName: "" # If not set, uses backend.imageName + imageName: ghcr.io/buoyantio/faces-color # imageTag: "" # If not set, uses backend.imageTag # imagePullPolicy: "" # If not set, uses backend.imagePullPolicy # errorFraction: "" # If not set, uses backend.errorFraction From 84428611bdf9eff38dde45a25158d61b564870f2 Mon Sep 17 00:00:00 2001 From: Flynn Date: Thu, 3 Oct 2024 23:48:32 -0400 Subject: [PATCH 4/4] Make sure the query row & column get propagated everywhere Signed-off-by: Flynn --- pkg/faces/faceserver.go | 6 +++--- pkg/faces/guiserver.go | 7 +++++++ pkg/faces/ingressserver.go | 6 ++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/faces/faceserver.go b/pkg/faces/faceserver.go index 23c2970..dba3290 100644 --- a/pkg/faces/faceserver.go +++ b/pkg/faces/faceserver.go @@ -88,10 +88,10 @@ func (srv *FaceServer) SetupFromEnvironment() { fmt.Printf("%s %s: colorService %v\n", time.Now().Format(time.RFC3339), srv.Name, srv.colorService) } -func (srv *FaceServer) makeRequest(user string, userAgent string, service string, keyword string, subrequest string) *FaceResponse { +func (srv *FaceServer) makeRequest(user string, userAgent string, service string, keyword string, subrequest string, row int, col int) *FaceResponse { start := time.Now() - url := fmt.Sprintf("http://%s/%s/", service, subrequest) + url := fmt.Sprintf("http://%s/%s/?row=%d&col=%d", service, subrequest, row, col) if srv.debugEnabled { fmt.Printf("%s %s: %s starting\n", time.Now().Format(time.RFC3339), srv.Name, url) @@ -265,7 +265,7 @@ func (srv *FaceServer) faceGetHandler(r *http.Request, rstat *BaseRequestStatus) colorCh := make(chan *FaceResponse) go func() { - smileyCh <- srv.makeRequest(user, userAgent, srv.smileyService, "smiley", subrequest) + smileyCh <- srv.makeRequest(user, userAgent, srv.smileyService, "smiley", subrequest, row, column) }() go func() { diff --git a/pkg/faces/guiserver.go b/pkg/faces/guiserver.go index 273bb77..75c25c2 100644 --- a/pkg/faces/guiserver.go +++ b/pkg/faces/guiserver.go @@ -133,6 +133,13 @@ func (srv *GUIServer) guiGetHandler(w http.ResponseWriter, r *http.Request) { reqStart := time.Now() url := fmt.Sprintf("http://face/%s", r.URL.Path[6:]) + + rq := r.URL.RawQuery + + if rq != "" { + url = fmt.Sprintf("%s?%s", url, rq) + } + user := r.Header.Get(srv.userHeaderName) if user == "" { user = "unknown" diff --git a/pkg/faces/ingressserver.go b/pkg/faces/ingressserver.go index 267a600..5a1bf87 100644 --- a/pkg/faces/ingressserver.go +++ b/pkg/faces/ingressserver.go @@ -83,6 +83,12 @@ func (srv *IngressServer) ingressGetHandler(r *http.Request, rstat *BaseRequestS } else { url := fmt.Sprintf("http://%s%s", srv.faceService, r.URL.Path) + rq := r.URL.RawQuery + + if rq != "" { + url = fmt.Sprintf("%s?%s", url, rq) + } + if srv.debugEnabled { fmt.Printf("%s %s: %s starting\n", time.Now().Format(time.RFC3339), srv.Name, url) }