Help us shape the future of Go! Take the 2026 Go Developer Survey

samples

package
v0.39.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package samples persists raw vehicle_state polling snapshots.

Index

Constants

View Source
const FullKeep = 7 * 24 * time.Hour

FullKeep is how long a thinned month's full partition stays (detached) so a bad thinning can be undone.

View Source
const MaxBatchMonths = 120

MaxBatchMonths is the most distinct months one batch may span (WindowStart to now is about 100).

View Source
const MaxDownsampleSpan = 7 * 24 * time.Hour

MaxDownsampleSpan bounds the window that is bucketed. The count and the bucketing scan every row in the window, so a caller-controlled span (anonymous demo callers included) must stay bounded; wider windows, and windows with no upper bound, keep the old cheap behaviour (the oldest `limit` rows, LIMIT stops the scan early).

View Source
const MinRetentionMonths = 3

MinRetentionMonths is the shortest retention accepted: anything shorter is almost certainly a typo, and dropping last month's samples by mistake can't be undone.

View Source
const MinThinAfterDays = 30

MinThinAfterDays is the youngest data thinning may touch: recent detail is what repairs and plan vs actual look at.

Variables

View Source
var (
	// WindowStart is the earliest sample time accepted: before Rivian's first
	// deliveries and before any tool that could export data.
	WindowStart = time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC)
	// WindowAhead is how far past now a sample may be dated (clock skew).
	WindowAhead = 24 * time.Hour
)

Which sample times the store accepts. vehicle_state is partitioned by month and writing a sample creates the month's partition, so a timestamp from the year 1 or 9999 would create a partition for it (an import with scattered dates could create thousands, each taking a lock on the parent). Every writer goes through prepareBatch, which refuses samples outside the window and batches that span too many months; the SQL helper has the same bounds.

View Source
var (
	// ErrSampleOutOfWindow reports a sample dated before WindowStart or more than
	// WindowAhead after now.
	ErrSampleOutOfWindow = errors.New("samples: a sample is dated outside the accepted window")
	// ErrTooManyMonths reports a batch spanning more than MaxBatchMonths months.
	ErrTooManyMonths = errors.New("samples: batch spans too many months")
)
View Source
var ErrBackupUnavailable = errors.New("backup unavailable: the request carries no user for the archive")

ErrBackupUnavailable means the archive cannot be read for this user: the context carries no user, or another one. A backup that went ahead would be missing months without saying so, so it fails.

View Source
var ErrNotArchived = errors.New("month not archived: needs a verified archive first")

ErrNotArchived refuses a thinning or a drop that would lose detail no archive holds. Under N19 a month is reduced only after every (user, vehicle) in it has a verified archive object (archive.MonthArchived). Dry runs don't need one.

View Source
var ErrThinVerify = errors.New("thinned copy failed verification")

ErrThinVerify reports a thinned copy that failed its checks; nothing was swapped.

Functions

func ParseRetentionMonths added in v0.37.6

func ParseRetentionMonths(v string) (int, error)

ParseRetentionMonths reads RIVOLT_SAMPLE_RETENTION_MONTHS: "" or "0" keeps samples forever (the default, so a self-hosted install keeps its history unless it opts in); otherwise at least MinRetentionMonths.

func ParseThinAfterDays added in v0.37.6

func ParseThinAfterDays(v string) (int, error)

ParseThinAfterDays reads RIVOLT_SAMPLE_THIN_AFTER_DAYS: "" or "0" never thins (the default); otherwise at least MinThinAfterDays.

func RetentionCutoff added in v0.37.6

func RetentionCutoff(now time.Time, months int) time.Time

RetentionCutoff is the first month whose partition is kept: partitions for months before it hold only rows older than months months, and go. Samples therefore live at least months months and less than months+1.

func ThinCandidate added in v0.37.6

func ThinCandidate(months []time.Time, thinned map[time.Time]bool, now time.Time, afterDays int) (time.Time, bool)

ThinCandidate picks the month to thin: the oldest month whose partition ends at or before now-afterDays and that is not yet thinned. months must be sorted oldest first.

func ThinCandidates added in v0.38.0

func ThinCandidates(months []time.Time, thinned map[time.Time]bool, now time.Time, afterDays int) []time.Time

ThinCandidates lists every month ThinCandidate would pick in turn, oldest first.

func TimeInWindow added in v0.37.5

func TimeInWindow(t, now time.Time) bool

TimeInWindow reports whether t is a sample time the store accepts at now.

Types

type ArchiveRows added in v0.37.7

type ArchiveRows interface {
	Objects(ctx context.Context, uid uuid.UUID) ([]archive.ObjectRef, error)
	EachRow(ctx context.Context, uid, vehicleID uuid.UUID, month time.Time, fn func(archive.Row) error) error
}

ArchiveRows is what the backup reads the telemetry archive through (archive.Reader).

type DayActivity added in v0.30.0

type DayActivity struct {
	Day        time.Time `json:"day"`
	AsleepH    float64   `json:"asleep_h"`
	IdleAwakeH float64   `json:"idle_awake_h"`
	// Sleeps counts the times the car fell asleep that day;
	// FailedSleeps the ones that ended within a minute. A healthy car
	// sleeps a few times a day for hours; a high failed share means
	// something keeps waking it, which is usually what drives
	// IdleAwakeH up.
	Sleeps       int `json:"sleeps"`
	FailedSleeps int `json:"failed_sleeps"`
}

DayActivity is one calendar day's (UTC) power-state breakdown, in hours, derived from the persisted power_state column (migration 0038). IdleAwakeH is the interesting one: the car awake while parked and not actively charging — i.e. staying awake for no obvious reason, the vampire-drain signal. Driving and active charging are excluded (both are expected awake time). Only intervals with a recorded power_state contribute, so the series fills forward from the deploy.

type Downsampled added in v0.33.0

type Downsampled struct {
	Samples []Sample
	// Reduced is true when the window held more than limit rows and the
	// series is a bucketed reduction of it.
	Reduced bool
	// Total is how many rows the window holds. Only known for a window that was
	// counted (bounded, within MaxDownsampleSpan); otherwise it is len(Samples).
	Total int
	// Truncated is true when the window was NOT bucketed (unbounded, or wider than
	// MaxDownsampleSpan) and the result reached the cap: the rows are the OLDEST
	// `limit` and the tail may be missing. It can be a false alarm when the window
	// holds exactly `limit` rows.
	Truncated bool
}

Downsampled describes the result of ListBetweenDownsampled.

type DriveCondition added in v0.36.10

type DriveCondition struct {
	ID           string
	VehicleID    string
	StartedAt    time.Time
	EndedAt      time.Time
	CreatedAt    time.Time
	Source       string
	DistanceMi   float64
	EnergyKWh    float64
	AvgSpeedMph  float64
	OutsideTempC *float64
	DriveMode    string
	// Trailer: a trailer was connected (the user's correction, else what was recorded).
	Trailer bool
}

DriveCondition is a recorded drive with the conditions it was driven in, from the drive's own samples: the median non-zero outside temperature (zero is the live merge's "no reading" sentinel, as in the drive recap) and the drive mode with the most samples.

type Factory

type Factory struct {
	// contains filtered or unexported fields
}

Factory hands out per-user *Store instances. See drives.Factory for the rationale.

func NewFactory

func NewFactory(d *sql.DB, resolvers *db.VehicleResolverFactory) *Factory

NewFactory wraps the shared pool + resolver factory.

func NewFactoryWithSystem added in v0.39.8

func NewFactoryWithSystem(d, sys *sql.DB, resolvers *db.VehicleResolverFactory) *Factory

NewFactoryWithSystem is NewFactory for request handling under row-level security: d is the request pool and sys the owner-role pool the stores use to create a month's partition.

func (*Factory) For

func (f *Factory) For(userID uuid.UUID) *Store

For returns the *Store for userID, constructing one on first sight.

type PartitionJanitor

type PartitionJanitor struct {
	// contains filtered or unexported fields
}

PartitionJanitor keeps `vehicle_state` ahead of the clock by at least `lookaheadMonths` partitions. It does this by calling the `rivolt_ensure_vehicle_state_partition(ts)` SQL helper once per target month.

func NewPartitionJanitor

func NewPartitionJanitor(db *sql.DB) *PartitionJanitor

NewPartitionJanitor wires a janitor with sensible defaults. 3 months of lookahead is enough that a pod that runs continuously for a month still never writes into an unpartitioned range, and a pod that restarts hourly (deploys, node drains) has slack for clock skew. The interval is also 1 hour — cheap, boring, and avoids the "I deployed at 23:59 UTC on the last of the month and the janitor fired at 23:30" edge.

func (*PartitionJanitor) EnsureLookahead

func (j *PartitionJanitor) EnsureLookahead(ctx context.Context) error

EnsureLookahead creates the current partition plus `lookaheadMonths` future partitions if they don't already exist. Idempotent; safe to call repeatedly. Returns the first error encountered but always attempts every month.

func (*PartitionJanitor) Run

func (j *PartitionJanitor) Run(ctx context.Context)

Run blocks until ctx is cancelled, calling EnsureLookahead on an `interval` ticker. Intended to be launched as a dedicated goroutine at startup. Errors are logged, never returned — the next tick will retry.

type Retention added in v0.37.6

type Retention struct {

	// Dropped, when set, is told each partition dropped with its row count
	// estimate (pg_class.reltuples): the caller logs and counts it.
	Dropped func(name string, estRows int64)
	// contains filtered or unexported fields
}

Retention drops vehicle_state partitions past the retention period.

func NewRetention added in v0.37.6

func NewRetention(db *sql.DB, months int) *Retention

NewRetention returns the sweep for months of retention, or nil for 0 (keep forever).

func (*Retention) Run added in v0.37.6

func (r *Retention) Run(ctx context.Context) (int64, error)

Run drops every partition before the cutoff, oldest first, and reports how many it dropped. One replica sweeps at a time; the others return 0. It stops at the first error; the rest wait for the next day's sweep.

type Sample

type Sample struct {
	VehicleID       string
	At              time.Time
	BatteryLevelPct float64
	RangeMi         float64
	OdometerMi      float64
	Lat, Lon        float64
	// LocationFixAt is the timestamp Rivian's GNSS module reported on
	// the (Lat, Lon) fix itself, NOT the wall-clock time the recorder
	// observed it. Differs from At when the modem loses sky view and
	// keeps replaying the last known fix — see migration 0013 for
	// the field rationale and the Fort Stockton incident that
	// motivated it. Pointer (rather than time.Time + omitempty)
	// because Go's encoding/json doesn't honor omitempty on struct
	// types: a zero time.Time would serialize as "0001-01-01T00:00:00Z"
	// and the frontend would compute a 2000-year fix age. nil
	// renders as "fix age unknown" upstream.
	LocationFixAt *time.Time `json:",omitempty"`
	SpeedMph      float64
	ShiftState    string
	DriveMode     string `json:"drive_mode,omitempty"` // "everyday", "sport", "all-terrain", etc.; nullable in DB for legacy samples
	ChargingState string
	// PowerState is the car's power/sleep state ("sleep" | "ready" |
	// "standby" | "go"). Empty on legacy rows / imports (NULL in DB).
	// Feeds the sleep/activity aggregation.
	PowerState     string `json:"power_state,omitempty"`
	ChargerPowerKW float64
	// ChargerStatus is Rivian's chargerStatus, the plug indicator
	// (migration 0053); "" when not reported.
	ChargerStatus  string `json:"charger_status,omitempty"`
	ChargeLimitPct float64
	InsideTempC    float64
	OutsideTempC   float64
	DriveNumber    int64
	ChargeNumber   int64
	Source         string // "live" | "electrafi_import"
	// AltitudeM is the elevation above sea level (meters) at
	// (Lat, Lon), looked up by the live recorder against the
	// Mapzen Terrarium DEM at sample-write time. Pointer because
	// it's nullable: NULL on legacy rows written before migration
	// 0019, on samples without a GPS fix, on ElectraFi imports,
	// and on cold-cache misses where the tile fetch was kicked
	// off async (the next sample on that tile will hit the cache).
	AltitudeM *float64 `json:"altitude_m,omitempty"`
	// TirePressureMinBar is the minimum of the four corner pressures
	// at sample time, in bar. Pointer because nullable: NULL on
	// legacy rows pre-migration 0024, ElectraFi imports, and live
	// rows where Rivian reported zero on all four corners (sensor
	// outage / vehicle parked w/ no recent reading). The efficiency
	// analyzer pulls the median across a drive's samples; min-of-4
	// keeps the worst tire (the one that actually hurts efficiency)
	// dominant without bloating the schema with all four corners.
	TirePressureMinBar *float64 `json:"tire_pressure_min_bar,omitempty"`
	// PackTempAvg/Max/MinC are high-voltage battery cell temperatures
	// (°C), merged in from the Parallax energy.high_voltage.battery_state
	// topic. Pointers because nullable: NULL on legacy rows pre-migration
	// 0036, imports, and live rows recorded before the vehicle pushed a
	// battery_state frame. Avg/max/min kept separately so the spread
	// (thermal imbalance) is queryable.
	PackTempAvgC *float64 `json:"pack_temp_avg_c,omitempty"`
	PackTempMaxC *float64 `json:"pack_temp_max_c,omitempty"`
	PackTempMinC *float64 `json:"pack_temp_min_c,omitempty"`
}

Sample is a single polling snapshot. VehicleID is the Rivian gateway string id ("01-000000000"); the store resolves it to the internal vehicles.id UUID on write.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store wraps the vehicle_state table, scoped to one user.

func OpenStore

func OpenStore(d *sql.DB, userID uuid.UUID, v *db.VehicleResolver) (*Store, error)

OpenStore binds a pooled connection to a user + vehicle resolver.

func (*Store) Close

func (s *Store) Close() error

Close is a no-op; the pool is managed by main.

func (*Store) Count

func (s *Store) Count(ctx context.Context) (int, error)

Count returns the total number of samples for this user.

func (*Store) DriveConditions added in v0.36.10

func (s *Store) DriveConditions(ctx context.Context, vehicleID string, since, until time.Time) ([]DriveCondition, error)

DriveConditions lists the user's drives that started in [since, until) (optionally one vehicle, by Rivian id) with their conditions. Drives under a minute are left out, as in the drive list. Each drive reads only its own window of vehicle_state (primary key vehicle_id, at).

func (*Store) EachAll added in v0.36.17

func (s *Store) EachAll(ctx context.Context, fn func(Sample) error) error

ListAll returns every sample for this user, oldest first. Used by the Settings → Backup endpoint. Unbounded — a year of 60 s polls is on the order of 500k rows, ~100 MB JSON; acceptable for a full backup but NOT suitable for UI consumption.

func (*Store) EachAllArchived added in v0.37.7

func (s *Store) EachAllArchived(ctx context.Context, src ArchiveRows, fn func(Sample) error) (reduced []string, err error)

EachAllArchived is EachAll with full detail for thinned months (N19): a month thinning has reduced is read from the user's archive objects, plus any of the month's rows in Postgres an object lacks (rows that came after it was archived, needs_rearchive or not). A thinned month with a vehicle that has no object, or whose object is missing from the store, falls back to the reduced rows in Postgres and is returned in reduced ("2026-05"), for the backup to say so. Months go oldest first; inside a thinned month, vehicle by vehicle, each by at. With src nil it is EachAll.

An archive object that fails after some of its rows were passed to fn (corrupt, truncated) is an error: the caller must fail the whole backup, not end it as if complete.

func (*Store) InsertBatch

func (s *Store) InsertBatch(ctx context.Context, batch []Sample) error

func (*Store) InsertBulk added in v0.32.0

func (s *Store) InsertBulk(ctx context.Context, batch []Sample) error

InsertBulk is InsertBatch for large batches: each chunk of up to bulkInsertRows samples is one multi-row INSERT (one round trip) instead of one EXEC per row. Same columns, same ON CONFLICT DO NOTHING, same partition and vehicle handling. Chunks commit independently; a failure returns the error and leaves earlier chunks written, which is safe because inserts are idempotent. Used by replay catch-up; the live recorder keeps InsertBatch.

func (*Store) ListAll

func (s *Store) ListAll(ctx context.Context) ([]Sample, error)

ListAll returns every row; use EachAll for large result sets.

func (*Store) ListBetween

func (s *Store) ListBetween(ctx context.Context, since, until time.Time, limit int) ([]Sample, error)

ListBetween returns samples in (since, until], oldest first. Zero `until` means "no upper bound" (back-compat with ListSince). The upper bound matters for the drive detail page: without it, opening any drive ages back pulls every sample from the drive's start through now, which can be days of charging+parked rows.

func (*Store) ListBetweenDownsampled added in v0.33.0

func (s *Store) ListBetweenDownsampled(ctx context.Context, since, until time.Time, limit int) (Downsampled, error)

ListBetweenDownsampled is ListBetween with the cap handled by bucketing instead of truncation. At or under the cap it returns every row exactly as ListBetween does. Without an upper bound, or over MaxDownsampleSpan, it IS ListBetween (never flagged Reduced).

func (*Store) ListBetweenDownsampledVehicle added in v0.33.2

func (s *Store) ListBetweenDownsampledVehicle(ctx context.Context, vehicleID string, since, until time.Time, limit int) (Downsampled, error)

ListBetweenDownsampledVehicle is ListBetweenDownsampled scoped to one vehicle (Rivian vehicle id); empty means every vehicle of the user. The total and the bucketing count only that vehicle's rows.

func (*Store) ListBetweenVehicle added in v0.33.2

func (s *Store) ListBetweenVehicle(ctx context.Context, vehicleID string, since, until time.Time, limit int) ([]Sample, error)

ListBetweenVehicle is ListBetween scoped to one vehicle (Rivian vehicle id). An empty vehicleID means every vehicle of the user; an id the user doesn't own matches nothing, so the result is empty, never another user's rows.

func (*Store) ListSince

func (s *Store) ListSince(ctx context.Context, since time.Time, limit int) ([]Sample, error)

ListSince returns samples newer than since, up to limit, oldest first.

Limit semantics:

  • <= 0 falls back to a reasonable default (1000).
  • Anything above the hard cap is clamped DOWN to the cap, not reset to the default. The previous "if limit > 10000 { limit = 1000 }" branch was a foot-gun: the UI asks for 20k samples so a 90-minute drive (~1.7k rows at the WS cadence) returns fully, but the store silently truncated to 1k → the route polyline ended halfway through the trip and the user reported "no GPS coordinates for the end of the route".

func (*Store) Reset

func (s *Store) Reset(ctx context.Context) (int64, error)

Reset deletes every raw sample (vehicle_state row) for this store's user. See drives.Store.Reset.

func (*Store) SleepActivity added in v0.30.0

func (s *Store) SleepActivity(ctx context.Context, vehicleID string, since, until time.Time) ([]DayActivity, error)

SleepActivity attributes each inter-sample interval to the vehicle's state at its start and sums per UTC day over [since, until). Intervals are capped at 20 min so a data outage never counts as one giant block — steady-state cadence stays well under that even while the car sleeps (the Rivian cloud replays keep the sample stream dense). vehicleID scopes to one car; empty aggregates all of the user's vehicles.

idle_awake = awake (power_state<>'sleep') AND parked (gear not D/R/N) AND not actively charging (charging_state not charging_active / charging_connecting). An unknown (NULL) gear or charging state counts as parked and not charging, as in internal/drain. That isolates unexplained awake time from normal driving / charging.

Charging is judged by charging_state alone, not charger_power_kw: until the recorder fix alongside this, the stored power kept the last session's value on every parked sample (see dropIdleChargerPower in internal/rivian), which made every awake hour look like charging and flattened idle-awake to zero. charging_state was always right, so keying on it corrects the history as well as new rows.

type ThinReport added in v0.37.6

type ThinReport struct {
	Partition               string
	RowsBefore, RowsAfter   int64
	BytesBefore, BytesAfter int64
	DryRun                  bool
}

ThinReport is one month's thinning.

type Thinner added in v0.37.6

type Thinner struct {

	// DryRun computes and reports what a run would keep, and changes nothing.
	DryRun bool
	// Now is the clock (tests set it).
	Now func() time.Time
	// Report, when set, is told each month processed: in a dry run what it
	// would keep, otherwise what it kept.
	Report func(r ThinReport)
	// OnRethin, when set, is told each restored (user, vehicle) month thinned
	// again once its hold (archive_months.rethin_after) ran out.
	OnRethin func(partition string, uid, vid uuid.UUID, deleted int64)
	// OnRepair, when set, is told how many objects were flagged for
	// re-archiving because their month's rows no longer matched them.
	OnRepair func(partition string, flagged int64)
	// contains filtered or unexported fields
}

Thinner thins old vehicle_state partitions.

func NewThinner added in v0.37.6

func NewThinner(db *sql.DB, afterDays int) *Thinner

NewThinner returns the thinning job for afterDays, or nil for 0 (off).

func (*Thinner) Run added in v0.37.6

func (t *Thinner) Run(ctx context.Context) (int64, error)

Run thins at most one month and drops full copies past FullKeep. It returns the number of partitions it thinned (0 or 1).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL