Staging mostly-finished code

This commit is contained in:
Donaldson 2021-10-28 15:36:07 -07:00
parent bb41697ba6
commit 56ea619b57
3 changed files with 296 additions and 1 deletions

View File

@ -7,6 +7,7 @@ COPY example-puzzles /target/puzzles/
COPY LICENSE.md /target/ COPY LICENSE.md /target/
RUN mkdir -p /target/state RUN mkdir -p /target/state
WORKDIR /src/ WORKDIR /src/
RUN go get github.com/go-redis/redis/v8
RUN CGO_ENABLED=0 GOOS=linux go install -a -ldflags '-extldflags "-static"' ./... RUN CGO_ENABLED=0 GOOS=linux go install -a -ldflags '-extldflags "-static"' ./...
# I can't use /target/bin: doing so would cause the devel server to overwrite Ubuntu's /bin # I can't use /target/bin: doing so would cause the devel server to overwrite Ubuntu's /bin

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"mime" "mime"
"os" "os"
"strconv"
"time" "time"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -52,6 +53,25 @@ func main() {
"", "",
"Random seed to use, overrides $SEED", "Random seed to use, overrides $SEED",
) )
stateEngine := flag.String(
"state-engine",
"legacy",
"Specifiy a state engine",
)
redis_url := flag.String(
"redis-url",
"",
"URL for Redis state instance",
)
redis_db := flag.Uint64(
"redis-db",
^uint64(0),
"Database number for Redis state instance",
)
flag.Parse() flag.Parse()
osfs := afero.NewOsFs() osfs := afero.NewOsFs()
@ -68,7 +88,34 @@ func main() {
} }
var state StateProvider var state StateProvider
state = NewState(afero.NewBasePathFs(osfs, *statePath))
switch engine := *stateEngine; engine {
case "redis":
redis_url_parsed := *redis_url
if redis_url_parsed == "" {
redis_url_parsed = os.Getenv("REDIS_URL")
if redis_url_parsed == "" {
log.Fatal("Redis mode was selected, but --redis-url or REDIS_URL were not set")
}
}
redis_db_parsed := *redis_db
if redis_db_parsed == ^uint64(0) {
redis_db_parsed_inner, err := strconv.ParseUint(os.Getenv("REDIS_DB"), 10, 64)
redis_db_parsed = redis_db_parsed_inner
if err != nil {
log.Fatal("Redis mode was selected, but --redis-db or REDIS_DB were not set")
}
}
state = NewRedisState(redis_url_parsed, int(redis_db_parsed))
default:
case "legacy":
state = NewState(afero.NewBasePathFs(osfs, *statePath))
}
if config.Devel { if config.Devel {
state = NewDevelState(state) state = NewDevelState(state)
} }

247
cmd/mothd/redis_state.go Normal file
View File

@ -0,0 +1,247 @@
package main
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/dirtbags/moth/pkg/award"
)
const (
REDIS_KEY_MESSAGE = "moth:message"
REDIS_KEY_TEAMS = "moth:teams"
REDIS_KEY_TEAM_IDS = "moth:team_ids"
REDIS_KEY_POINT_LOG = "moth:points"
)
type RedisState struct {
ctx context.Context
redis_client *redis.Client
// Enabled tracks whether the current State system is processing updates
Enabled bool
eventStream chan []string
//lock sync.RWMutex
}
type RedisEventEntry struct {
/*
[]string{
strconv.FormatInt(time.Now().Unix(), 10),
event,
participantID,
teamID,
cat,
strconv.Itoa(points),
},
extra...,*/
// Unix epoch time of this event
When int64
Event string
ParticipantID string
TeamID string
Category string
Points int
Extra []string
}
// MarshalJSON returns the award event, encoded as a list.
func (e RedisEventEntry) MarshalJSON() ([]byte, error) {
/*ao := []interface{}{
a.When,
a.TeamID,
a.Category,
a.Points,
}*/
return json.Marshal(e)
}
// UnmarshalJSON decodes the JSON string b.
func (e RedisEventEntry) UnmarshalJSON(b []byte) error {
//r := bytes.NewReader(b)
//dec := json.NewDecoder(r)
//dec.UseNumber() // Don't use floats
json.Unmarshal(b, &e)
return nil
}
// NewRedisState returns a new State struct backed by the given Fs
func NewRedisState(redis_addr string, redis_db int) *RedisState {
rdb := redis.NewClient(&redis.Options{
Addr: redis_addr,
Password: "", // no password set
DB: redis_db, // use default DB
})
s := &RedisState{
Enabled: true,
eventStream: make(chan []string, 80),
ctx: context.Background(),
redis_client: rdb,
}
return s
}
// Messages retrieves the current messages.
func (s *RedisState) Messages() string {
val, err := s.redis_client.Get(s.ctx, REDIS_KEY_MESSAGE).Result()
if err != nil {
return ""
}
return val
}
// TeamName returns team name given a team ID.
func (s *RedisState) TeamName(teamID string) (string, error) {
team_name, err := s.redis_client.HGet(s.ctx, REDIS_KEY_TEAMS, teamID).Result()
if err != nil {
return "", fmt.Errorf("unregistered team ID: %s", teamID)
}
return team_name, nil
}
// SetTeamName writes out team name.
// This can only be done once per team.
func (s *RedisState) SetTeamName(teamID, teamName string) error {
valid_id, err := s.redis_client.SIsMember(s.ctx, REDIS_KEY_TEAM_IDS, teamID).Result()
if err != nil {
return fmt.Errorf("Unexpected error while validating team ID: %s", teamID)
} else if (!valid_id) {
return fmt.Errorf("team ID: (%s) not found in list of valid team IDs", teamID)
}
success, err := s.redis_client.HSetNX(s.ctx, REDIS_KEY_TEAMS, teamID, teamName).Result()
if err != nil {
return fmt.Errorf("Unexpected error while setting team ID: %s and team Name: %s", teamID, teamName)
}
if (success) {
return nil
}
return fmt.Errorf("Team ID: %s is already set", teamID)
}
// PointsLog retrieves the current points log.
func (s *RedisState) PointsLog() award.List {
redis_args := &redis.ZRangeBy{
Min: "0",
Max: "-1",
}
scores, err := s.redis_client.ZRangeByScoreWithScores(s.ctx, REDIS_KEY_POINT_LOG, redis_args).Result()
if err != nil {
return make(award.List, 0)
}
point_log := make(award.List, len(scores))
for _, item := range scores {
point_entry := award.T{}
point_string := strings.TrimSpace(item.Member.(string))
point_entry.When = int64(item.Score)
n, err := fmt.Sscanf(point_string, "%s %s %d", &point_entry.TeamID, &point_entry.Category, &point_entry.Points)
if err != nil {
// Do nothing
} else if n != 3 {
// Wrong number of fields, do nothing
} else {
point_log = append(point_log, point_entry)
}
}
return point_log
}
func (s *RedisState) AwardPoints(teamID, category string, points int) error {
redis_args := redis.ZAddArgs {
LT: true,
}
awardTime := time.Now().Unix()
point_string := fmt.Sprintf("%s %s %s", teamID, category, points)
new_member := redis.Z{
Score: float64(awardTime),
Member: point_string,
}
redis_args.Members = append(redis_args.Members, new_member)
_, err := s.redis_client.ZAddArgs(s.ctx, REDIS_KEY_POINT_LOG, redis_args).Result()
if err != nil {
return err
}
return nil
}
// LogEvent writes to the event log
func (s *RedisState) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
new_event := RedisEventEntry {
When: time.Now().Unix(),
Event: event,
ParticipantID: participantID,
TeamID: teamID,
Category: cat,
Points: points,
Extra: extra,
}
message := new_event.MarshalJSON()
/*
s.eventStream <- append(
[]string{
strconv.FormatInt(time.Now().Unix(), 10),
event,
participantID,
teamID,
cat,
strconv.Itoa(points),
},
extra...,
)*/
}
func (s *RedisState) Maintain(updateInterval time.Duration) {
/*
ticker := time.NewTicker(updateInterval)
s.refresh()
for {
select {
case msg := <-s.eventStream:
s.eventWriter.Write(msg)
s.eventWriter.Flush()
s.eventWriterFile.Sync()
case <-ticker.C:
s.refresh()
case <-s.refreshNow:
s.refresh()
}
}
*/
}