forked from retoor/devplacepy
Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant, autoreplant claim or Refactor. Autoreplant now also respects crop locks through the shared _crop_lock predicate. Plan and measurements in happyplanting.md. Also includes the pending community sync, node network and related work in the tree; the full suite passed (4999) with all of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+43
-9
@@ -25,10 +25,11 @@ func (e *FatalError) Error() string {
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
fatal string
|
||||
fast bool
|
||||
stable bool
|
||||
reason string
|
||||
fatal string
|
||||
fast bool
|
||||
stable bool
|
||||
reason string
|
||||
holdUntil time.Time
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
@@ -40,6 +41,8 @@ type Session struct {
|
||||
log *Logger
|
||||
httpc *http.Client
|
||||
jitter func() float64
|
||||
now func() time.Time
|
||||
sleep func(ctx context.Context, d time.Duration)
|
||||
dialWS func(ctx context.Context, rawURL string) (*Conn, error)
|
||||
limits ServerLimits
|
||||
maxBody int
|
||||
@@ -55,6 +58,8 @@ func NewSession(cfg Config, policy *Policy, limiter *Limiter, fetcher *Fetcher,
|
||||
log: logger,
|
||||
httpc: &http.Client{Timeout: time.Duration(TicketTimeoutSeconds) * time.Second, Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}},
|
||||
jitter: rand.Float64,
|
||||
now: time.Now,
|
||||
sleep: sleepContext,
|
||||
dialWS: Dial,
|
||||
limits: defaultLimits(),
|
||||
maxBody: DefaultMaxBodyBytes,
|
||||
@@ -101,6 +106,21 @@ func sleepContext(ctx context.Context, d time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) holdDelay(until time.Time) time.Duration {
|
||||
wait := until.Sub(s.now())
|
||||
wait = max(wait, time.Duration(SuspendedMinSeconds)*time.Second)
|
||||
wait = min(wait, time.Duration(SuspendedMaxSeconds)*time.Second)
|
||||
return wait + time.Duration(s.jitter()*float64(SuspendedJitterSeconds)*float64(time.Second))
|
||||
}
|
||||
|
||||
func (s *Session) suspension(until string) outcome {
|
||||
end := s.now().Add(time.Duration(SuspendedDefaultSeconds) * time.Second)
|
||||
if moment, err := time.Parse(time.RFC3339, until); err == nil {
|
||||
end = moment
|
||||
}
|
||||
return outcome{reason: "device is suspended", holdUntil: end}
|
||||
}
|
||||
|
||||
func (s *Session) Run(ctx context.Context) error {
|
||||
attempt := 0
|
||||
for ctx.Err() == nil {
|
||||
@@ -115,6 +135,13 @@ func (s *Session) Run(ctx context.Context) error {
|
||||
attempt = 0
|
||||
}
|
||||
var delay time.Duration
|
||||
if !result.holdUntil.IsZero() {
|
||||
delay = s.holdDelay(result.holdUntil)
|
||||
attempt = 0
|
||||
s.log.Printf("device is suspended until %s, waiting %s", result.holdUntil.UTC().Format(time.RFC3339), delay.Round(time.Second))
|
||||
s.sleep(ctx, delay)
|
||||
continue
|
||||
}
|
||||
if result.fast {
|
||||
delay = time.Duration((0.5 + s.jitter()) * float64(FastRetryMillis) * float64(time.Millisecond))
|
||||
} else {
|
||||
@@ -122,7 +149,7 @@ func (s *Session) Run(ctx context.Context) error {
|
||||
attempt++
|
||||
}
|
||||
s.log.Printf("disconnected (%s), reconnecting in %s", result.reason, delay.Round(100*time.Millisecond))
|
||||
sleepContext(ctx, delay)
|
||||
s.sleep(ctx, delay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -136,6 +163,7 @@ type errorAnswer struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
Until string `json:"until"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
@@ -166,6 +194,9 @@ func (s *Session) fetchTicket(ctx context.Context) (ticketAnswer, outcome) {
|
||||
var failure errorAnswer
|
||||
_ = json.Unmarshal(body, &failure)
|
||||
reason := failure.Error.Reason
|
||||
if reason == SuspendedReason {
|
||||
return answer, s.suspension(failure.Error.Until)
|
||||
}
|
||||
if FatalReasons[reason] {
|
||||
return answer, outcome{fatal: reason}
|
||||
}
|
||||
@@ -292,13 +323,13 @@ func (s *Session) converse(ctx context.Context, conn *Conn) outcome {
|
||||
return s.protocolFailure(conn, "welcome is not valid json")
|
||||
}
|
||||
if welcome.Type == "kick" {
|
||||
return s.kickOutcome(welcome.Reason)
|
||||
return s.kickOutcome(welcome.Reason, welcome.Until)
|
||||
}
|
||||
if welcome.Type != "welcome" {
|
||||
return s.protocolFailure(conn, "expected welcome, got "+welcome.Type)
|
||||
}
|
||||
s.applyWelcome(welcome)
|
||||
s.log.Debugf("connected as device %s with %d allowed destinations", welcome.DeviceUID, len(s.policy.Destinations()))
|
||||
s.log.Debugf("connected as device %s (%s) with %d allowed destinations", welcome.DeviceUID, welcome.DeviceLabel, len(s.policy.Destinations()))
|
||||
|
||||
var jobs sync.WaitGroup
|
||||
tunnels := NewTunnels(s.policy, s.limiter, s.stats, send, s.cfg.MaxTunnels)
|
||||
@@ -342,7 +373,7 @@ func (s *Session) converse(ctx context.Context, conn *Conn) outcome {
|
||||
case "settings_update":
|
||||
s.limiter.ApplyServer(nil, frame.Settings)
|
||||
case "kick":
|
||||
return s.kickOutcome(frame.Reason)
|
||||
return s.kickOutcome(frame.Reason, frame.Until)
|
||||
default:
|
||||
return s.protocolFailure(conn, "unexpected frame "+frame.Type)
|
||||
}
|
||||
@@ -355,7 +386,10 @@ func (s *Session) protocolFailure(conn *Conn, reason string) outcome {
|
||||
return outcome{reason: "protocol error: " + reason}
|
||||
}
|
||||
|
||||
func (s *Session) kickOutcome(reason string) outcome {
|
||||
func (s *Session) kickOutcome(reason, until string) outcome {
|
||||
if reason == SuspendedReason {
|
||||
return s.suspension(until)
|
||||
}
|
||||
if FatalReasons[reason] {
|
||||
return outcome{fatal: reason}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user