From 335acd5b8227206f307c4a2672ac57a5b6f2f7e5 Mon Sep 17 00:00:00 2001 From: Alistair Hey Date: Fri, 24 Mar 2023 15:28:36 +0000 Subject: [PATCH] Add day 85 - Queues Workers and Tasks Signed-off-by: Alistair Hey --- 2023.md | 4 +- 2023/day2-ops-code/async/generator/Dockerfile | 17 +++ 2023/day2-ops-code/async/generator/go.mod | 17 +++ 2023/day2-ops-code/async/generator/go.sum | 32 ++++++ 2023/day2-ops-code/async/generator/main.go | 100 ++++++++++++++++ 2023/day2-ops-code/async/k8s.yaml | 69 +++++++++++ 2023/day2-ops-code/async/requestor/Dockerfile | 17 +++ 2023/day2-ops-code/async/requestor/go.mod | 17 +++ 2023/day2-ops-code/async/requestor/go.sum | 32 ++++++ 2023/day2-ops-code/async/requestor/main.go | 108 ++++++++++++++++++ 2023/day2-ops-code/buildpush.sh | 2 + 2023/day85.md | 62 ++++++++++ 2023/images/day84-queues.png | Bin 0 -> 34828 bytes 13 files changed, 475 insertions(+), 2 deletions(-) create mode 100644 2023/day2-ops-code/async/generator/Dockerfile create mode 100644 2023/day2-ops-code/async/generator/go.mod create mode 100644 2023/day2-ops-code/async/generator/go.sum create mode 100644 2023/day2-ops-code/async/generator/main.go create mode 100644 2023/day2-ops-code/async/k8s.yaml create mode 100644 2023/day2-ops-code/async/requestor/Dockerfile create mode 100644 2023/day2-ops-code/async/requestor/go.mod create mode 100644 2023/day2-ops-code/async/requestor/go.sum create mode 100644 2023/day2-ops-code/async/requestor/main.go create mode 100755 2023/day2-ops-code/buildpush.sh create mode 100644 2023/images/day84-queues.png diff --git a/2023.md b/2023.md index a380151..3fca4bc 100644 --- a/2023.md +++ b/2023.md @@ -156,8 +156,8 @@ Or contact us via Twitter, my handle is [@MichaelCade1](https://twitter.com/Mich ### Engineering for Day 2 Ops -- [βœ”] πŸ‘·πŸ»β€β™€οΈ 84 > [Writing an API - What is an API?](2023/day84.md) -- [] πŸ‘·πŸ»β€β™€οΈ 85 > [](2023/day85.md) +- [] πŸ‘·πŸ»β€β™€οΈ 84 > [Writing an API - What is an API?](2023/day84.md) +- [] πŸ‘·πŸ»β€β™€οΈ 85 > [Queues, Queue workers and Tasks (Asynchronous architecture)](2023/day85.md) - [] πŸ‘·πŸ»β€β™€οΈ 86 > [](2023/day86.md) - [] πŸ‘·πŸ»β€β™€οΈ 87 > [](2023/day87.md) - [] πŸ‘·πŸ»β€β™€οΈ 88 > [](2023/day88.md) diff --git a/2023/day2-ops-code/async/generator/Dockerfile b/2023/day2-ops-code/async/generator/Dockerfile new file mode 100644 index 0000000..9e40abf --- /dev/null +++ b/2023/day2-ops-code/async/generator/Dockerfile @@ -0,0 +1,17 @@ +# Set the base image to use +FROM golang:1.17-alpine + +# Set the working directory inside the container +WORKDIR /app + +# Copy the source code into the container +COPY . . + +# Build the Go application +RUN go build -o main . + +# Expose the port that the application will run on +EXPOSE 8080 + +# Define the command that will run when the container starts +CMD ["/app/main"] diff --git a/2023/day2-ops-code/async/generator/go.mod b/2023/day2-ops-code/async/generator/go.mod new file mode 100644 index 0000000..85f3760 --- /dev/null +++ b/2023/day2-ops-code/async/generator/go.mod @@ -0,0 +1,17 @@ +module main + +go 1.20 + +require ( + github.com/go-sql-driver/mysql v1.7.0 + github.com/nats-io/nats.go v1.24.0 +) + +require ( + github.com/golang/protobuf v1.5.3 // indirect + github.com/nats-io/nats-server/v2 v2.9.15 // indirect + github.com/nats-io/nkeys v0.3.0 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.6.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect +) diff --git a/2023/day2-ops-code/async/generator/go.sum b/2023/day2-ops-code/async/generator/go.sum new file mode 100644 index 0000000..89ed185 --- /dev/null +++ b/2023/day2-ops-code/async/generator/go.sum @@ -0,0 +1,32 @@ +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= +github.com/nats-io/nats-server/v2 v2.9.15 h1:MuwEJheIwpvFgqvbs20W8Ish2azcygjf4Z0liVu2I4c= +github.com/nats-io/nats-server/v2 v2.9.15/go.mod h1:QlCTy115fqpx4KSOPFIxSV7DdI6OxtZsGOL1JLdeRlE= +github.com/nats-io/nats.go v1.24.0 h1:CRiD8L5GOQu/DcfkmgBcTTIQORMwizF+rPk6T0RaHVQ= +github.com/nats-io/nats.go v1.24.0/go.mod h1:dVQF+BK3SzUZpwyzHedXsvH3EO38aVKuOPkkHlv5hXA= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/2023/day2-ops-code/async/generator/main.go b/2023/day2-ops-code/async/generator/main.go new file mode 100644 index 0000000..49234e3 --- /dev/null +++ b/2023/day2-ops-code/async/generator/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "database/sql" + "fmt" + _ "github.com/go-sql-driver/mysql" + nats "github.com/nats-io/nats.go" + "math/rand" + "time" +) + +func generateAndStoreString() (string, error) { + // Connect to the database + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + if err != nil { + return "", err + } + defer db.Close() + + // Generate a random string + // Define a string of characters to use + characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // Generate a random string of length 10 + randomString := make([]byte, 64) + for i := range randomString { + randomString[i] = characters[rand.Intn(len(characters))] + } + + // Insert the random number into the database + _, err = db.Exec("INSERT INTO generator_async(random_string) VALUES(?)", string(randomString)) + if err != nil { + return "", err + } + + fmt.Printf("Random string %s has been inserted into the database\n", string(randomString)) + return string(randomString), nil +} + +func main() { + err := createGeneratordb() + if err != nil { + panic(err.Error()) + } + + nc, _ := nats.Connect("nats://my-nats:4222") + defer nc.Close() + + nc.Subscribe("generator", func(msg *nats.Msg) { + s, err := generateAndStoreString() + if err != nil { + print(err) + } + nc.Publish("generator_reply", []byte(s)) + nc.Publish("confirmation", []byte(s)) + }) + + nc.Subscribe("confirmation_reply", func(msg *nats.Msg) { + stringReceived(string(msg.Data)) + }) + // Subscribe to the queue + // when a message comes in call generateAndStoreString() then put the string on the + // reply queue. also add a message onto the confirmation queue + + // subscribe to the confirmation reply queue + // when a message comes in call + + for { + time.Sleep(1 * time.Second) + } + +} + +func createGeneratordb() error { + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + if err != nil { + return err + } + defer db.Close() + + // try to create a table for us + _, err = db.Exec("CREATE TABLE IF NOT EXISTS generator_async(random_string VARCHAR(100), seen BOOLEAN, requested BOOLEAN)") + + return err +} + +func stringReceived(input string) { + + // Connect to the database + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + if err != nil { + print(err) + } + defer db.Close() + + _, err = db.Exec("UPDATE generator_async SET requested = true WHERE random_string = ?", input) + if err != nil { + print(err) + } +} diff --git a/2023/day2-ops-code/async/k8s.yaml b/2023/day2-ops-code/async/k8s.yaml new file mode 100644 index 0000000..e77dd6c --- /dev/null +++ b/2023/day2-ops-code/async/k8s.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: requestor +spec: + replicas: 1 + selector: + matchLabels: + app: requestor + template: + metadata: + labels: + app: requestor + spec: + containers: + - name: requestor + image: heyal/requestor:async + imagePullPolicy: Always + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: requestor-service +spec: + selector: + app: requestor + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: generator +spec: + replicas: 1 + selector: + matchLabels: + app: generator + template: + metadata: + labels: + app: generator + spec: + containers: + - name: generator + image: heyal/generator:async + imagePullPolicy: Always + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: generator-service +spec: + selector: + app: generator + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP diff --git a/2023/day2-ops-code/async/requestor/Dockerfile b/2023/day2-ops-code/async/requestor/Dockerfile new file mode 100644 index 0000000..9e40abf --- /dev/null +++ b/2023/day2-ops-code/async/requestor/Dockerfile @@ -0,0 +1,17 @@ +# Set the base image to use +FROM golang:1.17-alpine + +# Set the working directory inside the container +WORKDIR /app + +# Copy the source code into the container +COPY . . + +# Build the Go application +RUN go build -o main . + +# Expose the port that the application will run on +EXPOSE 8080 + +# Define the command that will run when the container starts +CMD ["/app/main"] diff --git a/2023/day2-ops-code/async/requestor/go.mod b/2023/day2-ops-code/async/requestor/go.mod new file mode 100644 index 0000000..85f3760 --- /dev/null +++ b/2023/day2-ops-code/async/requestor/go.mod @@ -0,0 +1,17 @@ +module main + +go 1.20 + +require ( + github.com/go-sql-driver/mysql v1.7.0 + github.com/nats-io/nats.go v1.24.0 +) + +require ( + github.com/golang/protobuf v1.5.3 // indirect + github.com/nats-io/nats-server/v2 v2.9.15 // indirect + github.com/nats-io/nkeys v0.3.0 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.6.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect +) diff --git a/2023/day2-ops-code/async/requestor/go.sum b/2023/day2-ops-code/async/requestor/go.sum new file mode 100644 index 0000000..89ed185 --- /dev/null +++ b/2023/day2-ops-code/async/requestor/go.sum @@ -0,0 +1,32 @@ +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= +github.com/nats-io/nats-server/v2 v2.9.15 h1:MuwEJheIwpvFgqvbs20W8Ish2azcygjf4Z0liVu2I4c= +github.com/nats-io/nats-server/v2 v2.9.15/go.mod h1:QlCTy115fqpx4KSOPFIxSV7DdI6OxtZsGOL1JLdeRlE= +github.com/nats-io/nats.go v1.24.0 h1:CRiD8L5GOQu/DcfkmgBcTTIQORMwizF+rPk6T0RaHVQ= +github.com/nats-io/nats.go v1.24.0/go.mod h1:dVQF+BK3SzUZpwyzHedXsvH3EO38aVKuOPkkHlv5hXA= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/2023/day2-ops-code/async/requestor/main.go b/2023/day2-ops-code/async/requestor/main.go new file mode 100644 index 0000000..a3a09b6 --- /dev/null +++ b/2023/day2-ops-code/async/requestor/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + _ "github.com/go-sql-driver/mysql" + nats "github.com/nats-io/nats.go" + "time" +) + +func storeString(input string) error { + // Connect to the database + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + defer db.Close() + // Insert the random number into the database + _, err = db.Exec("INSERT INTO requestor_async(random_string) VALUES(?)", input) + if err != nil { + return err + } + + fmt.Printf("Random string %s has been inserted into the database\n", input) + return nil +} + +func getStringFromDB(input string) error { + // see if the string exists in the db, if so return nil + // if not, return an error + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + defer db.Close() + result, err := db.Query("SELECT * FROM requestor_async WHERE random_string = ?", input) + if err != nil { + return err + } + + for result.Next() { + var randomString string + err = result.Scan(&randomString) + if err != nil { + return err + } + if randomString == input { + return nil + } + } + + return errors.New("string not found") +} + +func main() { + + err := createRequestordb() + if err != nil { + panic(err.Error()) + } + // setup a goroutine loop calling the generator every minute, saving the result in the DB + + nc, _ := nats.Connect("nats://my-nats:4222") + defer nc.Close() + + ticker := time.NewTicker(60 * time.Second) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + nc.Publish("generator", []byte("")) + case <-quit: + ticker.Stop() + return + } + } + }() + + nc.Subscribe("generator_reply", func(msg *nats.Msg) { + err := storeString(string(msg.Data)) + if err != nil { + print(err) + } + }) + + nc.Subscribe("confirmation", func(msg *nats.Msg) { + err := getStringFromDB(string(msg.Data)) + if err != nil { + print(err) + } + nc.Publish("confirmation_reply", []byte(string(msg.Data))) + }) + // create a goroutine here to listen for messages on the queue to check, see if we have them + + for { + time.Sleep(10 * time.Second) + } + +} + +func createRequestordb() error { + db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql") + if err != nil { + return err + } + defer db.Close() + + // try to create a table for us + _, err = db.Exec("CREATE TABLE IF NOT EXISTS requestor_async(random_string VARCHAR(100))") + + return err +} diff --git a/2023/day2-ops-code/buildpush.sh b/2023/day2-ops-code/buildpush.sh new file mode 100755 index 0000000..5a25b3f --- /dev/null +++ b/2023/day2-ops-code/buildpush.sh @@ -0,0 +1,2 @@ +docker build ./async/requestor/ -f async/requestor/Dockerfile -t heyal/requestor:async && docker push heyal/requestor:async +docker build ./async/generator/ -f async/generator/Dockerfile -t heyal/generator:async&& docker push heyal/generator:async \ No newline at end of file diff --git a/2023/day85.md b/2023/day85.md index e69de29..c9e05a6 100644 --- a/2023/day85.md +++ b/2023/day85.md @@ -0,0 +1,62 @@ +# Queues, Queue workers and Tasks (Asynchronous architecture) +Yesterday we looked at how we can use HTTP based APIs for communication between services. This works well until you need +to scale, release a new version or one of your services goes down. Then we start to see the calling service fail because +it’s dependency is not working as expected. We have tightly coupled our two services, one can’t work without the other. + +There are many ways to solve this problem, a light touch approach for existing applications is to use something called a +circuit breaker to buffer failures and retry until we get a successful response. This is explained well in this blog +by [Martin Fowler](https://martinfowler.com/bliki/CircuitBreaker.html). However, this is synchronous, if we were to wrap +our calls in a circuit breaker we would start to block processes and our user could see a slowdown in response times. + +Additionally, we can’t scale our applications using this approach, the way that the code is currently written every +instance of our `generator` api would be asking +the `requestor for confirmation of receiving the string. This won’t scale well when we move to having 2, 5, 10, or 100 instances running. We would quickly see the ` +requestor` being overwhelmed with requests from the 100 generator applications. + +There is a way to solve these problems which is to use Queues. This is a shift in thinking to using an asynchronous +approach to solving our problem. This can work well when the responses don’t need to be immediate between applications. +In this case it doesn't matter if we add some delay in the requests between the applications. As long as the data +eventually flows between them we are happy. + +![Queues, producers and Consumers](./images/day84-queues.png) +(https://dashbird.io/knowledge-base/well-architected/queue-and-asynchronous-processing/) + +In the drawing above we can see how we can add a Queue in between our applications and the Queue stores the intent of +the message across the bridge. If the Consumer was to fail and stop reacting messages then, providing our Queue software +has sufficient storage, the messages to the consumer would still β€œsucceed” as far as the producer is concerned. + +This is a powerful pattern that isolates components of a system from each other and limits the blast radius of failure. +It does however add complexity. The Consumer and Producer must share a common understanding of the shape of a message +otherwise the Consumer will be unable to act on the message. + +We are going to implement a Queue in between our two applications in our data flows. + +By the end of the section we will have the following data flows + +Requestor (asks for a random string) β†’ Queue β†’ Generator (gets the message, generates a string and passes it back to the +Requestor on another Queue) β†’ Requestor (gets the string back on a queue and stores it, then sends a message to the +queue saying it has received it) β†’ Queue β†’ Generator (marks that the message was received) + +The last section here, where the Generator needs to know if the Requestor got the message is a simplification of a real +process where an application may pass back an enriched data record or some further information but it allows us to have +a simple two way communication. + +Can you see how by the end of this section we will be able to stop, scale, deploy or modify each of the two components +without the other needing to know? + +## Modifying our application + +We are now going to modify our app to fit the model described above. Where we previously made a HTTP api call to our +partner app we are now going to place a message on a named queue and then subscribe to the corresponding response +queues. + +Finally we are going to stop using HTTP endpoints to listen for requests, we are now subscribing to queues and waiting +for messages before we perform any work. + +I have picked [NATSio](https://nats.io/) as the queue technology as I have worked with it previously and it’s very easy +to set up and use. + +Head over to the 2023/day2-ops-code/README.md to see how to deploy day 2's application. + +It sends messages to a queue and then waits for a response. The response is a message on a different queue. The message +contains the data that we are interested in. \ No newline at end of file diff --git a/2023/images/day84-queues.png b/2023/images/day84-queues.png new file mode 100644 index 0000000000000000000000000000000000000000..3ba5e3f3e7eb0f30354aadf697c1409d30e25b43 GIT binary patch literal 34828 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 literal 0 HcmV?d00001