updates on the ai agents and time series prediction and updates on the api to
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -31,6 +32,35 @@ type Client interface {
|
||||
GeoAdd(ctx context.Context, key string, geoLocation ...*redis.GeoLocation) *redis.IntCmd
|
||||
GeoSearchLocation(ctx context.Context, key string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd
|
||||
GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *redis.GeoRadiusQuery) *redis.GeoLocationCmd
|
||||
ZRem(ctx context.Context, key string, members ...interface{}) *redis.IntCmd
|
||||
}
|
||||
|
||||
// Remove drops a rider from the GEO set.
|
||||
//
|
||||
// ─── Why this has to exist ─────────────────────────────────────────────────
|
||||
//
|
||||
// A GEO set member never expires. Nothing removed a rider when they went off
|
||||
// duty, so `milers:locations` accumulated every rider who had ever started a
|
||||
// shift, frozen at their last reported position — for good.
|
||||
//
|
||||
// That is not merely untidy, because assignment asks for the TEN NEAREST
|
||||
// members (geoMaxCount in internal/assignment). Riders who finished weeks ago,
|
||||
// parked near the hub where most pickups originate, are geographically the
|
||||
// closest members there are. They fill all ten slots, every one of them is
|
||||
// then discarded by the GPS-freshness check, and the candidate pool collapses
|
||||
// to whoever happened to survive — often a single rider.
|
||||
//
|
||||
// The balancing logic below that (leastLoaded, betterChoice) is correct and
|
||||
// irrelevant: it can only balance across the pool it is handed. The symptom is
|
||||
// every order in a city going to the same person.
|
||||
//
|
||||
// Called on end-duty. A failure is logged by the caller and not fatal: a rider
|
||||
// left in the set is the status quo, not a regression.
|
||||
func Remove(ctx context.Context, rdb Client, milerUserID int) error {
|
||||
if rdb == nil {
|
||||
return nil
|
||||
}
|
||||
return rdb.ZRem(ctx, Key, strconv.Itoa(milerUserID)).Err()
|
||||
}
|
||||
|
||||
// legacyOnly flips to true the first time the server rejects GEOSEARCH as an
|
||||
|
||||
@@ -17,6 +17,19 @@ type fakeRedis struct {
|
||||
locs []redis.GeoLocation
|
||||
searchHits int
|
||||
radiusHits int
|
||||
remErr error
|
||||
removed []interface{}
|
||||
}
|
||||
|
||||
func (f *fakeRedis) ZRem(ctx context.Context, _ string, members ...interface{}) *redis.IntCmd {
|
||||
f.removed = append(f.removed, members...)
|
||||
cmd := redis.NewIntCmd(ctx)
|
||||
if f.remErr != nil {
|
||||
cmd.SetErr(f.remErr)
|
||||
} else {
|
||||
cmd.SetVal(int64(len(members)))
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (f *fakeRedis) GeoSearchLocation(ctx context.Context, _ string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd {
|
||||
@@ -118,3 +131,45 @@ func TestNilClientIsRefused(t *testing.T) {
|
||||
t.Fatalf("probe = %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
// A GEO member never expires, and nothing removed one when a rider went off
|
||||
// duty — so milers:locations kept every rider who had ever started a shift,
|
||||
// frozen where they last reported. Assignment takes the N NEAREST members, so
|
||||
// those stale entries crowded out riders who were actually working and the
|
||||
// candidate pool collapsed to whoever survived the freshness filter.
|
||||
func TestRemoveDropsTheRiderFromTheSet(t *testing.T) {
|
||||
f := &fakeRedis{}
|
||||
if err := Remove(context.Background(), f, 412); err != nil {
|
||||
t.Fatalf("Remove: %v", err)
|
||||
}
|
||||
if len(f.removed) != 1 || f.removed[0] != "412" {
|
||||
t.Errorf("removed = %v, want [\"412\"]", f.removed)
|
||||
}
|
||||
}
|
||||
|
||||
// The member is the rider's userid as a DECIMAL STRING — the same spelling
|
||||
// GeoAdd writes and Search reads back. A mismatch here would remove nothing
|
||||
// and report success.
|
||||
func TestRemoveUsesTheSameMemberSpellingAsAdd(t *testing.T) {
|
||||
f := &fakeRedis{}
|
||||
_ = Remove(context.Background(), f, 7)
|
||||
if f.removed[0] != "7" {
|
||||
t.Errorf("member = %v, want \"7\" (decimal string, not an int)", f.removed[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort at the call site: the rider is off duty either way, and the
|
||||
// freshness check still excludes them. The error must reach the caller so it
|
||||
// can be logged rather than swallowed here.
|
||||
func TestRemoveReturnsTheError(t *testing.T) {
|
||||
f := &fakeRedis{remErr: errors.New("redis down")}
|
||||
if err := Remove(context.Background(), f, 1); err == nil {
|
||||
t.Error("Remove swallowed the error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveOnNilClientIsANoOp(t *testing.T) {
|
||||
if err := Remove(context.Background(), nil, 1); err != nil {
|
||||
t.Errorf("Remove(nil) = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user