new exporter version
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 1: ping" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="ping" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 2: get/set" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="getset" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 3: expire" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="expire" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 4: pipeline" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="pipeline" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 5: transaction" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="transaction" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo 6: pub/sub" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="pubsub" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo: pipeline benchmark" type="GoTestRunConfiguration" factoryName="Go Test">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="DIRECTORY" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<framework value="gobench" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="RedisDemo: reset data" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="get-started-with-redis" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="reset" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
# Getting Started with Redis in Go
|
||||
|
||||
This tutorial demonstrates how to use Redis, the popular in-memory database that doubles as cache, pub/sub service, and streaming service, with Go and GoLand.
|
||||
|
||||
Find the tutorial [here]().
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"time"
|
||||
)
|
||||
|
||||
func expiringKeys(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a temporary player
|
||||
err := client.HSet(ctx, "player:10", "name", "Crymyios", "score", 0, "team", "Knucklewimp", "challenges_completed", 0).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot set player:10: %w", err)
|
||||
}
|
||||
|
||||
// Set an expiration time for player:10
|
||||
if !client.Expire(ctx, "player:10", time.Second).Val() {
|
||||
return fmt.Errorf("cannot set expiration time for player:10")
|
||||
}
|
||||
|
||||
// Get player:10
|
||||
for i := 0; i < 3; i++ {
|
||||
val, err := client.HGet(ctx, "player:10", "name").Result()
|
||||
if err != nil {
|
||||
fmt.Printf("player:10 has expired: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("player:10's name: %s\n", val)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Task: The quest is a lowercase string. Change it to title case.
|
||||
// Then print the challenges in the order they have to be completed.
|
||||
|
||||
func getAndSet(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
quest, err := client.Get(ctx, "quest").Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get quest: %w", err)
|
||||
}
|
||||
|
||||
quest = cases.Title(language.English).String(quest)
|
||||
|
||||
err = client.Set(ctx, "quest", quest, 0).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update quest: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Quest is now: %s\n", client.Get(ctx, "quest").Val())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module github.com/JetBrains/go-code-samples/get-started-with-redis
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
golang.org/x/text v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
dbconn = "localhost:6379"
|
||||
db = 0
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Println(`Usage: go run main.go <demo>
|
||||
Where <demo> is one of:
|
||||
- ping: run the ping command
|
||||
- getset: get and set a string value
|
||||
- expire: set an expiring key
|
||||
- pipeline: run a batch of commands
|
||||
- transaction: run a batch of commands that must all succeed
|
||||
- pubsub: send a message to a channel and listen to the channel
|
||||
- reset: restore the initial data set`)
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// Create a new client from a connection string and a database number (0-15)
|
||||
client := newClient(dbconn, 0)
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
return nil
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "ping":
|
||||
// Ping the redis server and fetch some database information.
|
||||
fmt.Printf("\nPing: Test the connection\n")
|
||||
ping(client)
|
||||
|
||||
case "getset":
|
||||
// Get and set a string value.
|
||||
fmt.Printf("\nGet/Set: Update the quest to title case\n")
|
||||
err := getAndSet(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAndSet failed: %w", err)
|
||||
}
|
||||
|
||||
case "expire":
|
||||
// Set an expiring key and wait for it to expire
|
||||
fmt.Printf("\nExpire: Add a player temporarily\n")
|
||||
err := expiringKeys(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expiringKeys failed: %w", err)
|
||||
}
|
||||
|
||||
case "pipeline":
|
||||
// Run a batch of commands.
|
||||
fmt.Printf("\nPipeline: Update score and challenges_completed for team Snarkdumbthimble\n")
|
||||
err := pipeline(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipeline failed: %w", err)
|
||||
}
|
||||
|
||||
case "transaction":
|
||||
// Run several commands that must all succeed.
|
||||
// If any of them fails, the transaction will be canceled.
|
||||
fmt.Printf("\nTransaction: Rearrange the teams\n")
|
||||
err := transaction(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transaction failed: %w", err)
|
||||
}
|
||||
|
||||
case "pubsub":
|
||||
// Send messages to a publish/receive channel.
|
||||
// Listen to the channel and receive the messages.
|
||||
fmt.Printf("\nPub/Sub: Publish challenges to subscribed teams\n")
|
||||
err := pubsub(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pubsub stopped: %w", err)
|
||||
}
|
||||
|
||||
case "reset":
|
||||
// Reset the database
|
||||
fmt.Printf("\nReset the database\n")
|
||||
err := resetdata(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset database: %w", err)
|
||||
}
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func ping(client *redis.Client) error {
|
||||
// For the demo, we need only a background context
|
||||
ctx := context.Background()
|
||||
// Ping the redis server. It should respond with "PONG".
|
||||
fmt.Println(client.Ping(ctx))
|
||||
|
||||
// Get the client info.
|
||||
info, err := client.ClientInfo(ctx).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("method ClientInfo failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", info)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Task: Update the score and the challenges_completed
|
||||
// for team Snarkdumbthimble that has finished challenge #1.
|
||||
|
||||
func pipeline(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
err := pipe.HSet(ctx, "player:7", "score", 15, "challenges_completed", 1).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pipe.HSet(ctx, "player:8", "score", 18, "challenges_completed", 1).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pipe.HSet(ctx, "player:9", "score", 12, "challenges_completed", 1).Err()
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pipelined failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Player 7's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:7", "score").Val(),
|
||||
client.HGet(ctx, "player:7", "challenges_completed").Val())
|
||||
fmt.Printf("Player 8's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:8", "score").Val(),
|
||||
client.HGet(ctx, "player:8", "challenges_completed").Val())
|
||||
fmt.Printf("Player 9's score: %s, challenges completed: %s\n",
|
||||
client.HGet(ctx, "player:9", "score").Val(),
|
||||
client.HGet(ctx, "player:9", "challenges_completed").Val())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func incrementScorePipe(client *redis.Client, player string) error {
|
||||
ctx := context.Background()
|
||||
client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
for i := 0; i < 1000; i++ {
|
||||
err := pipe.HIncrBy(ctx, player, "score", 1).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
|
||||
}
|
||||
}
|
||||
pipe.HSet(ctx, player, "score", 1)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func incrementScoreNoPipe(client *redis.Client, player string) error {
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 1000; i++ {
|
||||
err := client.HIncrBy(ctx, player, "score", 1).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
|
||||
}
|
||||
}
|
||||
client.HSet(ctx, player, "score", 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func BenchmarkPipeline(b *testing.B) {
|
||||
client := newClient(dbconn, 0)
|
||||
b.Run("PipelinedHIncrBy", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
incrementScorePipe(client, "player:1")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkNoPipeline(b *testing.B) {
|
||||
client := newClient(dbconn, 0)
|
||||
b.Run("HIncrBy", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
incrementScoreNoPipe(client, "player:2")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task: Send each challenge to the "challenge" channel.
|
||||
// Each team is subscribed to the "challenge" channel and enters the challenge upon receiving.
|
||||
|
||||
const (
|
||||
// the name of the PubSub channel
|
||||
pubsubChan = "challenge"
|
||||
)
|
||||
|
||||
// Res is the result of reading the pubsub channel
|
||||
type Res struct {
|
||||
result string
|
||||
err error
|
||||
}
|
||||
|
||||
// Team manages a team's subscription
|
||||
// Each team uses its own client to subscribe to the "challenge" channel
|
||||
type Team struct {
|
||||
name string
|
||||
client *redis.Client
|
||||
channel *redis.PubSub
|
||||
}
|
||||
|
||||
// getTeams scans the database for "team:*" keys
|
||||
// and returns a slice of Team structs with names filled from the keys
|
||||
func getTeams(client *redis.Client) []Team {
|
||||
ctx := context.Background()
|
||||
teams := make([]Team, 3)
|
||||
teamsets := make([]string, 0, 3)
|
||||
keys := make([]string, 0, 3)
|
||||
var cursor uint64
|
||||
for {
|
||||
// Scan returns a slice of matches. The count may or may not be reached
|
||||
// in the first call to Scan, so the code needs to call Scan in a loop and
|
||||
// append the found keys to the teamsets slice until the cursor "returns to 0".
|
||||
var err error
|
||||
keys, cursor, err = client.Scan(ctx, cursor, "team:*", 3).Result()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
teamsets = append(teamsets, keys...)
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Lazily assume that the scan has returned 3 team sets
|
||||
for i := 0; i < 3; i++ {
|
||||
teams[i].name = teamsets[i]
|
||||
// each team uses its own client
|
||||
teams[i].client = newClient(dbconn, 0)
|
||||
}
|
||||
return teams
|
||||
}
|
||||
|
||||
// subscribe subscribes to the "challenge" channel
|
||||
// and waits for the subscription to be completed
|
||||
func (team *Team) subscribe() error {
|
||||
ctx := context.Background()
|
||||
// Subscribe to the "challenge" channel
|
||||
pubSub := team.client.Subscribe(ctx, pubsubChan)
|
||||
|
||||
// The first Subscribe() call creates the channel.
|
||||
// Until that point, any attempt to publish something fails.
|
||||
reply, err := pubSub.Receive(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribing to channel '%s' failed: %w", pubsubChan, err)
|
||||
}
|
||||
// Expected response type is "*Subscription". Otherwise, something failed.
|
||||
switch reply.(type) {
|
||||
case *redis.Subscription:
|
||||
// Success!
|
||||
case *redis.Message:
|
||||
// The channel is already active and contains messages, hence also a success
|
||||
case *redis.Pong:
|
||||
// letL's call it a success
|
||||
default:
|
||||
return fmt.Errorf("subscribing to a channel failed: received a reply of type %T, expected: *redis.Subscription", reply)
|
||||
}
|
||||
|
||||
team.channel = pubSub
|
||||
|
||||
fmt.Printf("%s subscribed to channel '%s'\n", team.name, pubsubChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// receive receives messages from the "challenge" channel.
|
||||
// It starts a goroutine that reads from the pubsub channel until
|
||||
// the channel is closed or the context is done.
|
||||
func (team *Team) receive(ctx context.Context, resChan chan<- Res) {
|
||||
ch := team.channel.Channel()
|
||||
defer close(resChan)
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
// The pubsub channel has been closed
|
||||
return
|
||||
}
|
||||
resChan <- Res{fmt.Sprintf("%s received challenge '%s'", team.name, msg.Payload), nil}
|
||||
case <-ctx.Done():
|
||||
resChan <- Res{"", ctx.Err()}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish publishes the challenge to the "challenge" channel
|
||||
func publish(client *redis.Client, challenge string) error {
|
||||
ctx := context.Background()
|
||||
fmt.Printf("publishing challenge '%s'\n", challenge)
|
||||
return client.Publish(ctx, pubsubChan, challenge).Err()
|
||||
}
|
||||
|
||||
// pubsub subscribes to the "challenge" channel, publishes the challenges,
|
||||
// and receives the published messages.
|
||||
func pubsub(client *redis.Client) (err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Step 1: subscribe each team
|
||||
teams := getTeams(client)
|
||||
for i := 0; i < 3; i++ {
|
||||
err = teams[i].subscribe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("subscribing failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: publish challenges
|
||||
// Read the challenges from the sorted set "challenges" and publish them
|
||||
for i := int64(0); i < 5; i++ {
|
||||
challenge := client.ZRange(ctx, "challenges", i, i).Val()[0]
|
||||
err = publish(client, challenge)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot publish challenge %s: %w", challenge, err)
|
||||
}
|
||||
}
|
||||
// Close the channel after one second, to terminate the receive loops.
|
||||
time.AfterFunc(time.Second, func() {
|
||||
teams[0].channel.Close()
|
||||
fmt.Println(`PubSub channel "challenges" closed`)
|
||||
})
|
||||
|
||||
// Step 3: receive published messages
|
||||
rch := make(chan Res)
|
||||
for i := 0; i < 3; i++ {
|
||||
go teams[i].receive(ctx, rch)
|
||||
}
|
||||
for msg := range rch {
|
||||
if msg.err != nil {
|
||||
return fmt.Errorf("cannot receive challenge: %w", msg.err)
|
||||
}
|
||||
fmt.Println(msg.result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import "github.com/redis/go-redis/v9"
|
||||
|
||||
func newClient(conn string, db int) *redis.Client {
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: conn,
|
||||
DB: db,
|
||||
Password: "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
setupfile = "testdata/setup.redis"
|
||||
)
|
||||
|
||||
// resetdata flushes all data from the current database (db 0)
|
||||
// and runs the commands from file setup.redis to set up the database.
|
||||
func resetdata(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// read file "setup.redis" line by line
|
||||
setup, err := os.Open(setupfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open %s: %w", setupfile, err)
|
||||
}
|
||||
defer setup.Close()
|
||||
|
||||
client.FlushDB(ctx) // FlushDB never fails.
|
||||
|
||||
csv := csv.NewReader(setup)
|
||||
csv.Comma = ' '
|
||||
csv.FieldsPerRecord = -1 // Variable number of fields per line
|
||||
|
||||
for {
|
||||
cmd, err := csv.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("csv: cannot read a line from %s: %w", setupfile, err)
|
||||
}
|
||||
|
||||
// cmd is a slice of strings, Do() expects a slice of 'any'.
|
||||
// The memory layout of the two slice types is not the same,
|
||||
// so we need to convert cmd to a slice of 'any'.
|
||||
doCmd := make([]interface{}, len(cmd))
|
||||
for i, v := range cmd {
|
||||
doCmd[i] = v
|
||||
}
|
||||
|
||||
err = client.Do(ctx, doCmd...).Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetdata: cannot execute '%v': %w", cmd, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
FLUSHDB
|
||||
SET quest "the chicken coop catastrophe"
|
||||
SET description "Players must rescue a group of chickens that have been taken hostage by a group of evil goblins."
|
||||
HSET "player:1" name Sykios score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:2" name Nidios score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:3" name Tiaitia score 0 team Dorkfoot challenges_completed 0
|
||||
HSET "player:4" name Belaeos score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:5" name Polytia score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:6" name Moritia score 0 team Knucklewimp challenges_completed 0
|
||||
HSET "player:7" name Daryos score 0 team Snarkdumbthimble challenges_completed 0
|
||||
HSET "player:8" name Blalios score 0 team Snarkdumbthimble challenges_completed 0
|
||||
HSET "player:9" name Ighteatia score 0 team Snarkdumbthimble challenges_completed 0
|
||||
SADD "team:Dorkfoot" Sykios Nidios Tiaitia
|
||||
SADD "team:Knucklewimp" Belaeos Polytia Moritia
|
||||
SADD "team:Snarkdumbthimble" Daryos Blalios Ighteatia
|
||||
ZADD "challenges" 1 "Enter the hidden dungeon" 2 "Find the chicken coop" 3 "Defeat the goblins" 4 "Rescue the chickens" 5 "Escape the dungeon"
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Task:
|
||||
// - Create a new team: Grumblebum
|
||||
// - Move Sykios, Nidios, and Belaeos to the new team
|
||||
// - Move Tiaitia to team Knucklewimp
|
||||
// - Remove team Dorkfoot
|
||||
|
||||
func transaction(client *redis.Client) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := client.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
// Move Sykios to team Grumblebum
|
||||
err := pipe.HSet(ctx, "player:1", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Nidios to team Grumblebum
|
||||
err = pipe.HSet(ctx, "player:2", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Belaeos to team Grumblebum
|
||||
err = pipe.HSet(ctx, "player:4", "team", "Grumblebum").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Move Tiaitia to team Knucklewimp
|
||||
err = pipe.HSet(ctx, "player:3", "team", "Knucklewimp").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Team update: remove Belaeos from team Knucklewimp
|
||||
err = pipe.SRem(ctx, "team:Knucklewimp", "Belaeos").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Team update: add Tiaitia to team Knucklewimp
|
||||
err = pipe.SAdd(ctx, "team:Knucklewimp", "Tiaitia").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add team Grumblebum
|
||||
err = pipe.SAdd(ctx, "team:Grumblebum", "Sykios", "Nidios", "Belaeos").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove team Dorkfoot. A set is removed by removing all elements.
|
||||
err = pipe.SRem(ctx, "team:Dorkfoot", "Sykios", "Nidios", "Tiaitia").Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("TxPipelined failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Sykios's new team: %s\n", client.HGet(ctx, "player:1", "team").Val())
|
||||
fmt.Printf("Belaeos's new team: %s\n", client.HGet(ctx, "player:4", "team").Val())
|
||||
fmt.Printf("Tiaitia's new team: %s\n", client.HGet(ctx, "player:3", "team").Val())
|
||||
fmt.Printf("Team Grumblebum: %s\n", client.SMembers(ctx, "team:Grumblebum").Val())
|
||||
fmt.Printf("Team Knucklewimp: %s\n", client.SMembers(ctx, "team:Knucklewimp").Val())
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user