forecast

package
v1.7.21 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package forecast projects a node's traffic for the rest of its billing cycle, and says when it would reach its plan limit.

Why this exists: hitting a traffic limit is currently discovered by hitting it, and for a NAT'd node that means the provider suspends it. The number is useful; the number's *basis* is what makes it actable, so every forecast here carries the method, the samples it used and an uncertainty rather than a bare figure.

The rule that shapes the design, and the first thing the acceptance criteria ask for: **a forecast that cannot be made is not made.** Extrapolating two points into a confident number is worse than saying "not enough data", because the number is what an operator acts on — and the failure mode of a wrong projection is either a suspension that was predicted as fine, or a plan upgrade that was never needed. So `Method` can be `insufficient`, and that is a first-class answer rather than an error.

Index

Constants

View Source
const DefaultThreshold = 0.9

DefaultThreshold is the warning fraction when the caller does not choose one.

Ninety percent, because the warning has to arrive while there is still time to act: at the limit itself the only remaining options are a plan upgrade or an outage, and a node that is suspended has no traffic to measure.

Variables

This section is empty.

Functions

This section is empty.

Types

type Basis

type Basis struct {
	// Samples is how many observations were used.
	Samples int `json:"samples"`
	// ObservedFor is how long the samples span.
	ObservedFor time.Duration `json:"observed_for"`
	// Coverage is the fraction of the cycle the samples span.
	Coverage float64 `json:"coverage"`
	// BytesPerDay is the observed average rate, which is the projection's slope.
	BytesPerDay float64 `json:"bytes_per_day"`
	// Uncertainty is the projection's relative error, from the spread of the per-interval rates.
	//
	// Estimated from the observed variance of the rate rather than asserted: a node with a steady
	// transfer has a small one and a node with a daily peak has a large one, and an operator deciding
	// whether to buy headroom needs to know which they are looking at. It is the relative standard
	// error of the mean rate times the projection's extrapolation factor, which is the honest
	// consequence of projecting further than you observed.
	Uncertainty float64 `json:"uncertainty"`
	// CycleDays is the cycle's length in days.
	CycleDays float64 `json:"cycle_days"`
}

Basis is the projection's own account of itself.

type Forecast

type Forecast struct {
	// Method is how the projection was reached, or MethodInsufficient.
	Method Method `json:"method"`
	// Reason explains an insufficient forecast, in the terms the operator needs: what was missing.
	Reason string `json:"reason,omitempty"`

	// UsedBytes is the observed total at Now, which is reported even when no projection is made: an
	// operator asking "how much have I used" deserves an answer even when the cycle is too young to
	// project.
	UsedBytes int64 `json:"used_bytes"`
	// ProjectedBytes is the cycle's projected end total. Zero when Method is insufficient.
	ProjectedBytes int64 `json:"projected_bytes"`

	// LimitSet is false when the node has no plan limit, in which case there is nothing to project
	// against and no crossing date to report.
	LimitSet bool `json:"limit_set"`
	// ProjectedFraction is ProjectedBytes / LimitBytes. Zero when no limit is set.
	ProjectedFraction float64 `json:"projected_fraction"`
	// WouldExceed is whether the projection reaches the limit before the cycle ends.
	WouldExceed bool `json:"would_exceed"`

	// CrossesAt is when the limit would be reached, and CrossesInDays how far away that is. Both are
	// zero when no crossing can be computed within time.Duration and JSON's year 0..9999 range.
	CrossesAt     time.Time `json:"crosses_at,omitempty"`
	CrossesInDays float64   `json:"crosses_in_days,omitempty"`
	// CrossesInCycle is whether the crossing falls inside this cycle. A crossing after the cycle ends
	// is not a problem this cycle, and saying so is the difference between a warning and noise.
	CrossesInCycle bool `json:"crosses_in_cycle"`

	// Basis is the arithmetic behind the projection, stated so a reader can check it.
	Basis Basis `json:"basis"`

	// Warning is set when the projection crosses the threshold and a notification is warranted.
	Warning *Warning `json:"warning,omitempty"`
}

Forecast is the answer.

func Compute

func Compute(now time.Time, request Request) Forecast

Compute projects the cycle's traffic and, when a limit is set, when it would be reached.

type Method

type Method string

Method names how a projection was reached.

const (
	// MethodInsufficient means no projection was made, and Reason says why.
	MethodInsufficient Method = "insufficient"
	// MethodLinearRate projects from the average rate observed so far.
	MethodLinearRate Method = "linear_rate"
)

type Request

type Request struct {
	// CycleStart and CycleEnd bound the billing cycle. The window is half-open, matching how the
	// ledger rotates: `[start, end)`.
	CycleStart time.Time
	CycleEnd   time.Time
	// Samples are the cycle's observations, in any order.
	Samples []Sample
	// LimitBytes is the plan's traffic allowance. Zero means no limit is set, and a node with no
	// limit is not forecast at all — see Forecast.LimitSet.
	LimitBytes int64
	// Threshold is the fraction of the limit at which a warning is worth raising, 0 < Threshold <= 1.
	// Zero means the default.
	Threshold float64
}

Request is what a forecast is computed from.

type Sample

type Sample struct {
	// At is when the total was observed.
	At time.Time
	// Total is the cycle-to-date traffic in bytes at that instant.
	Total int64
}

Sample is one observed traffic total for the cycle, in bytes.

A cumulative total rather than a delta, because the projection is about where the total will end up: the caller has the ledger's month-to-date value, and deltas would need re-summing here to say anything about the end of the cycle.

type Warning

type Warning struct {
	// Threshold is the fraction that was crossed.
	Threshold float64 `json:"threshold"`
	// ProjectedFraction is the projection's fraction of the limit.
	ProjectedFraction float64 `json:"projected_fraction"`
	// Message is a sentence for a human.
	Message string `json:"message"`
}

Warning is what the notifier sends.

Jump to

Keyboard shortcuts

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