Skip to content

Commit d634f2e

Browse files
authored
Merge pull request #444 from domino14/volunteer-mode-fixes
Fix volunteer mode: configurable Woogles URL, proto-JSON serializatio…
2 parents 097f7ca + 56ccf1a commit d634f2e

File tree

5 files changed

+91
-73
lines changed

5 files changed

+91
-73
lines changed

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
ConfigOpenaiBaseURL = "openai-base-url"
4242
ConfigDeepseekApiKey = "deepseek-api-key"
4343
ConfigWooglesApiKey = "woogles-api-key"
44+
ConfigWooglesURL = "woogles-url"
4445
ConfigAliases = "aliases"
4546
)
4647

@@ -114,6 +115,7 @@ func (c *Config) Load(args []string) error {
114115
c.BindEnv(ConfigOpenaiApiKey)
115116
c.BindEnv(ConfigDeepseekApiKey)
116117
c.BindEnv(ConfigWooglesApiKey)
118+
c.BindEnv(ConfigWooglesURL)
117119

118120
cfgdir, err := os.UserConfigDir()
119121
if err != nil {
@@ -167,6 +169,7 @@ func (c *Config) Load(args []string) error {
167169
c.SetDefault(ConfigOpenaiApiKey, "")
168170
c.SetDefault(ConfigDeepseekApiKey, "")
169171
c.SetDefault(ConfigWooglesApiKey, "")
172+
c.SetDefault(ConfigWooglesURL, "https://woogles.io")
170173

171174
return nil
172175
}

shell/shell.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ type ShellController struct {
203203
// Volunteer mode state
204204
volunteerMode bool
205205
volunteerStop bool // Signal to stop after current job
206+
volunteerBusy bool // True while processing a job
206207
volunteerCtx context.Context
207208
volunteerCancel context.CancelFunc
208209
}

shell/volunteer.go

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"time"
88

99
"github.com/rs/zerolog/log"
10-
"google.golang.org/protobuf/proto"
10+
"google.golang.org/protobuf/encoding/protojson"
1111

1212
"github.com/domino14/macondo/config"
1313
"github.com/domino14/macondo/gameanalysis"
@@ -17,7 +17,6 @@ import (
1717
const (
1818
VolunteerPollInterval = 30 * time.Second
1919
VolunteerHeartbeatInterval = 30 * time.Second
20-
WooglesBaseURL = "https://woogles.io"
2120
)
2221

2322
// volunteer handles the volunteer command
@@ -62,81 +61,98 @@ func (sc *ShellController) startVolunteer() (*Response, error) {
6261
return msg("Volunteer mode started. Polling for jobs every 30 seconds...\nUse 'volunteer stop' to exit gracefully."), nil
6362
}
6463

65-
// stopVolunteer signals volunteer mode to stop after current job
64+
// stopVolunteer stops volunteer mode, immediately if idle or after current job if busy
6665
func (sc *ShellController) stopVolunteer() (*Response, error) {
6766
if !sc.volunteerMode {
6867
return msg("Not in volunteer mode."), nil
6968
}
7069

71-
sc.volunteerStop = true
72-
return msg("Volunteer mode will stop after current job completes."), nil
70+
if sc.volunteerBusy {
71+
sc.volunteerStop = true
72+
return msg("Volunteer mode will stop after current job completes."), nil
73+
}
74+
75+
sc.volunteerCancel()
76+
return msg("Volunteer mode stopped."), nil
7377
}
7478

7579
// volunteerLoop polls for jobs and processes them
7680
func (sc *ShellController) volunteerLoop() {
7781
// Get API key and create client
7882
apiKey := sc.config.GetString(config.ConfigWooglesApiKey)
79-
client := worker.NewWooglesClient(WooglesBaseURL, apiKey)
83+
wooglesURL := sc.config.GetString(config.ConfigWooglesURL)
84+
client := worker.NewWooglesClient(wooglesURL, apiKey, sc.config)
8085

8186
ticker := time.NewTicker(VolunteerPollInterval)
8287
defer ticker.Stop()
8388

8489
log.Info().Msg("volunteer loop started")
8590

86-
for {
87-
select {
88-
case <-sc.volunteerCtx.Done():
89-
log.Info().Msg("volunteer loop cancelled")
91+
poll := func() bool {
92+
// Check if we should stop
93+
if sc.volunteerStop {
94+
log.Info().Msg("volunteer stop requested")
9095
sc.cleanupVolunteerMode()
91-
return
96+
return false
97+
}
9298

93-
case <-ticker.C:
94-
// Check if we should stop
95-
if sc.volunteerStop {
96-
log.Info().Msg("volunteer stop requested")
97-
sc.cleanupVolunteerMode()
98-
return
99-
}
99+
// Try to claim a job
100+
job, err := client.ClaimJob(sc.volunteerCtx)
101+
if err != nil {
102+
log.Warn().Err(err).Msg("failed to claim job")
103+
writeln(fmt.Sprintf("Warning: Failed to claim job: %v", err), sc.l.Stdout())
104+
return true
105+
}
100106

101-
// Try to claim a job
102-
job, err := client.ClaimJob(sc.volunteerCtx)
103-
if err != nil {
104-
log.Warn().Err(err).Msg("failed to claim job")
105-
writeln(fmt.Sprintf("Warning: Failed to claim job: %v", err), sc.l.Stdout())
106-
continue
107-
}
107+
if job == nil {
108+
log.Info().Msg("no jobs available, polling again in 30s")
109+
writeln("No jobs available, polling again in 30s...", sc.l.Stdout())
110+
return true
111+
}
108112

109-
if job == nil {
110-
// No jobs available
111-
log.Debug().Msg("no jobs available")
112-
continue
113-
}
113+
log.Info().
114+
Str("job-id", job.JobID).
115+
Str("game-id", job.GameID).
116+
Msg("claimed job")
117+
writeln(fmt.Sprintf("Claimed job %s for game %s", job.JobID, job.GameID), sc.l.Stdout())
114118

115-
// Got a job! Process it
119+
sc.volunteerBusy = true
120+
if err := sc.processVolunteerJob(client, job); err != nil {
121+
log.Error().
122+
Err(err).
123+
Str("job-id", job.JobID).
124+
Msg("failed to process job")
125+
writeln(fmt.Sprintf("Error processing job: %v", err), sc.l.Stdout())
126+
} else {
116127
log.Info().
117128
Str("job-id", job.JobID).
118-
Str("game-id", job.GameID).
119-
Msg("claimed job")
120-
writeln(fmt.Sprintf("Claimed job %s for game %s", job.JobID, job.GameID), sc.l.Stdout())
121-
122-
// Process the job
123-
if err := sc.processVolunteerJob(client, job); err != nil {
124-
log.Error().
125-
Err(err).
126-
Str("job-id", job.JobID).
127-
Msg("failed to process job")
128-
writeln(fmt.Sprintf("Error processing job: %v", err), sc.l.Stdout())
129-
} else {
130-
log.Info().
131-
Str("job-id", job.JobID).
132-
Msg("job completed successfully")
133-
writeln(fmt.Sprintf("Job %s completed successfully", job.JobID), sc.l.Stdout())
134-
}
129+
Msg("job completed successfully")
130+
writeln(fmt.Sprintf("Job %s completed successfully", job.JobID), sc.l.Stdout())
131+
}
132+
sc.volunteerBusy = false
133+
134+
if sc.volunteerStop {
135+
log.Info().Msg("volunteer stop requested after job completion")
136+
sc.cleanupVolunteerMode()
137+
return false
138+
}
139+
return true
140+
}
141+
142+
// Poll immediately on start
143+
if !poll() {
144+
return
145+
}
135146

136-
// After processing, check if we should stop
137-
if sc.volunteerStop {
138-
log.Info().Msg("volunteer stop requested after job completion")
139-
sc.cleanupVolunteerMode()
147+
for {
148+
select {
149+
case <-sc.volunteerCtx.Done():
150+
log.Info().Msg("volunteer loop cancelled")
151+
sc.cleanupVolunteerMode()
152+
return
153+
154+
case <-ticker.C:
155+
if !poll() {
140156
return
141157
}
142158
}
@@ -256,11 +272,10 @@ func (sc *ShellController) processVolunteerJob(client *worker.WooglesClient, job
256272
Int("turns-analyzed", len(result.Turns)).
257273
Msg("analysis complete")
258274

259-
// Convert result to protobuf
275+
// Convert result to protobuf and serialize as proto-JSON
260276
resultProto := result.ToProto()
261277

262-
// Serialize to bytes
263-
resultBytes, err := proto.Marshal(resultProto)
278+
resultBytes, err := protojson.Marshal(resultProto)
264279
if err != nil {
265280
return fmt.Errorf("failed to marshal result: %w", err)
266281
}

worker/client.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ type WooglesClient struct {
1919
baseURL string
2020
apiKey string
2121
httpClient *http.Client
22+
cfg *config.Config
2223
}
2324

2425
// NewWooglesClient creates a new Woogles API client
25-
func NewWooglesClient(baseURL, apiKey string) *WooglesClient {
26+
func NewWooglesClient(baseURL, apiKey string, cfg *config.Config) *WooglesClient {
2627
return &WooglesClient{
2728
baseURL: baseURL,
2829
apiKey: apiKey,
2930
httpClient: &http.Client{},
31+
cfg: cfg,
3032
}
3133
}
3234

@@ -63,17 +65,17 @@ func (c *WooglesClient) ClaimJob(ctx context.Context) (*Job, error) {
6365

6466
// Parse Connect RPC JSON response
6567
var claimResp struct {
66-
NoJobs bool `json:"noJobs"`
67-
JobId string `json:"jobId"`
68-
GameId string `json:"gameId"`
68+
NoJobs bool `json:"no_jobs"`
69+
JobId string `json:"job_id"`
70+
GameId string `json:"game_id"`
6971
Config struct {
70-
SimPlaysEarlyMid int32 `json:"simPlaysEarlyMid"`
71-
SimPliesEarlyMid int32 `json:"simPliesEarlyMid"`
72-
SimStopEarlyMid int32 `json:"simStopEarlyMid"`
73-
SimPlaysEarlyPreendgame int32 `json:"simPlaysEarlyPreendgame"`
74-
SimPliesEarlyPreendgame int32 `json:"simPliesEarlyPreendgame"`
75-
SimStopEarlyPreendgame int32 `json:"simStopEarlyPreendgame"`
76-
PegEarlyCutoff bool `json:"pegEarlyCutoff"`
72+
SimPlaysEarlyMid int32 `json:"sim_plays_early_mid"`
73+
SimPliesEarlyMid int32 `json:"sim_plies_early_mid"`
74+
SimStopEarlyMid int32 `json:"sim_stop_early_mid"`
75+
SimPlaysEarlyPreendgame int32 `json:"sim_plays_early_preendgame"`
76+
SimPliesEarlyPreendgame int32 `json:"sim_plies_early_preendgame"`
77+
SimStopEarlyPreendgame int32 `json:"sim_stop_early_preendgame"`
78+
PegEarlyCutoff bool `json:"peg_early_cutoff"`
7779
Threads int32 `json:"threads"`
7880
} `json:"config"`
7981
}
@@ -252,10 +254,7 @@ func (c *WooglesClient) FetchGameHistory(ctx context.Context, gameID string) (*p
252254
return nil, fmt.Errorf("failed to unmarshal GCG response: %w", err)
253255
}
254256

255-
// Parse GCG into GameHistory (need a config for parsing, but we don't need full config here)
256-
// Use a minimal config just for parsing
257-
parseConfig := &config.Config{}
258-
history, err := gcgio.ParseGCGFromReader(parseConfig, strings.NewReader(gcgObj.GCG))
257+
history, err := gcgio.ParseGCGFromReader(c.cfg, strings.NewReader(gcgObj.GCG))
259258
if err != nil {
260259
return nil, fmt.Errorf("failed to parse GCG: %w", err)
261260
}

worker/worker.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"time"
77

88
"github.com/rs/zerolog/log"
9-
"google.golang.org/protobuf/proto"
9+
"google.golang.org/protobuf/encoding/protojson"
1010

1111
"github.com/domino14/macondo/gameanalysis"
1212
)
@@ -20,7 +20,7 @@ type AnalysisWorker struct {
2020

2121
// NewAnalysisWorker creates a new worker
2222
func NewAnalysisWorker(cfg *WorkerConfig) *AnalysisWorker {
23-
client := NewWooglesClient(cfg.WooglesBaseURL, cfg.APIKey)
23+
client := NewWooglesClient(cfg.WooglesBaseURL, cfg.APIKey, cfg.MacondoConfig)
2424

2525
// Create analyzer with default config if none provided
2626
analysisConfig := gameanalysis.DefaultAnalysisConfig()
@@ -149,7 +149,7 @@ func (w *AnalysisWorker) processJob(ctx context.Context, job *Job) error {
149149
resultProto := result.ToProto()
150150

151151
// Serialize to bytes
152-
resultBytes, err := proto.Marshal(resultProto)
152+
resultBytes, err := protojson.Marshal(resultProto)
153153
if err != nil {
154154
return fmt.Errorf("failed to marshal result: %w", err)
155155
}

0 commit comments

Comments
 (0)