stillbox/pkg/database/partman/partman.go

370 lines
7.6 KiB
Go
Raw Normal View History

2024-11-28 10:11:46 -05:00
package partman
import (
"context"
2024-11-29 19:22:50 -05:00
"fmt"
2024-11-28 10:11:46 -05:00
"strconv"
"strings"
"time"
"dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database"
"github.com/jackc/pgx/v5"
2024-11-29 19:22:50 -05:00
"github.com/jackc/pgx/v5/pgtype"
2024-11-28 10:11:46 -05:00
"github.com/rs/zerolog/log"
)
2024-11-29 19:22:50 -05:00
const (
callsTable = "calls"
preProvisionDefault = 1
)
/*
* Partition scheme names:
* daily: calls_p_2024_11_28
* weekly: calls_p_2024_w48
* monthly: calls_p_2024_11
* quarterly: calls_p_2024_q4
* yearly: calls_p_2024
*/
2024-11-28 10:11:46 -05:00
type PartitionError string
func (pe PartitionError) Error() string {
return fmt.Sprintf("bad partition '%s'", string(pe))
}
type ErrInvalidInterval string
2024-11-29 19:22:50 -05:00
func (in ErrInvalidInterval) Error() string {
return fmt.Sprintf("invalid interval '%s'", string(in))
}
2024-11-28 10:11:46 -05:00
type Interval string
2024-11-29 19:22:50 -05:00
2024-11-28 10:11:46 -05:00
const (
2024-11-29 19:22:50 -05:00
Daily Interval = "daily"
Weekly Interval = "weekly"
Monthly Interval = "monthly"
2024-11-28 10:11:46 -05:00
Quarterly Interval = "quarterly"
2024-11-29 19:22:50 -05:00
Yearly Interval = "yearly"
2024-11-28 10:11:46 -05:00
)
func (p Interval) IsValid() bool {
switch p {
case Daily, Weekly, Monthly, Quarterly, Yearly:
return true
}
return false
}
type PartitionManager interface {
Go(ctx context.Context)
2024-11-29 19:22:50 -05:00
Check(ctx context.Context, now time.Time) error
2024-11-28 10:11:46 -05:00
}
type partman struct {
2024-11-29 19:22:50 -05:00
db database.Store
cfg config.Partition
2024-11-28 10:11:46 -05:00
intv Interval
}
type partition interface {
PartitionName() string
2024-11-29 19:22:50 -05:00
Next(i int) partition
Prev(i int) partition
Range() (time.Time, time.Time)
2024-11-28 10:11:46 -05:00
}
type monthlyPartition struct {
2024-11-29 19:22:50 -05:00
t time.Time
2024-11-28 10:11:46 -05:00
}
func (d monthlyPartition) PartitionName() string {
2024-11-29 19:22:50 -05:00
return fmt.Sprintf("calls_p_%d_%02d", d.t.Year(), d.t.Month())
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func (d monthlyPartition) Next(i int) partition {
return d.next(i)
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func (d monthlyPartition) Prev(i int) partition {
return d.prev(i)
}
func (d monthlyPartition) Range() (start, end time.Time) {
start = time.Date(d.t.Year(), d.t.Month(), 1, 0, 0, 0, 0, time.UTC)
end = start.AddDate(0, 1, 0)
return
}
func (d monthlyPartition) next(i int) monthlyPartition {
year, month, _ := d.t.Date()
2024-11-28 10:11:46 -05:00
2024-11-29 19:22:50 -05:00
return monthlyPartition{
t: time.Date(year, month+time.Month(i), 1, 0, 0, 0, 0, time.UTC),
}
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func (d monthlyPartition) prev(i int) monthlyPartition {
year, month, _ := d.t.Date()
return monthlyPartition{
t: time.Date(year, month-time.Month(i), 1, 0, 0, 0, 0, d.t.Location()),
}
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func New(db database.Store, cfg config.Partition) (*partman, error) {
2024-11-28 10:11:46 -05:00
pm := &partman{
2024-11-29 19:22:50 -05:00
cfg: cfg,
db: db,
2024-11-28 10:11:46 -05:00
intv: Interval(cfg.Interval),
}
if !pm.intv.IsValid() {
return nil, ErrInvalidInterval(pm.intv)
}
return pm, nil
}
var _ PartitionManager = (*partman)(nil)
func (pm *partman) Go(ctx context.Context) {
go func(ctx context.Context) {
2024-11-29 19:22:50 -05:00
tick := time.NewTicker(60 * time.Minute)
2024-11-28 10:11:46 -05:00
select {
2024-11-29 19:22:50 -05:00
case now := <-tick.C:
err := pm.Check(ctx, now)
2024-11-28 10:11:46 -05:00
if err != nil {
log.Error().Err(err).Msg("partman check failed")
}
case <-ctx.Done():
return
}
}(ctx)
}
func (pm *partman) newPartition(t time.Time) partition {
2024-11-29 19:22:50 -05:00
switch pm.intv {
case Monthly:
return monthlyPartition{t}
}
return nil
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func (pm *partman) retentionPartitions(cur partition) []partition {
partitions := make([]partition, 0, pm.cfg.Retain)
for i := 1; i <= pm.cfg.Retain; i++ {
prev := cur.Prev(i)
partitions = append(partitions, prev)
}
return partitions
}
func (pm *partman) futurePartitions(cur partition) []partition {
preProv := preProvisionDefault
if pm.cfg.PreProvision != nil {
preProv = *pm.cfg.PreProvision
}
partitions := make([]partition, 0, pm.cfg.Retain)
for i := 1; i <= preProv; i++ {
next := cur.Next(i)
partitions = append(partitions, next)
}
return partitions
}
func (pm *partman) expectedPartitions(now time.Time) []partition {
curPart := pm.newPartition(now)
retain := pm.retentionPartitions(curPart)
future := pm.futurePartitions(curPart)
shouldExist := append(retain, curPart)
shouldExist = append(shouldExist, future...)
return shouldExist
}
func (pm *partman) comparePartitions(existingTables, expectedTables []partition) (unexpectedTables, missingTables []partition) {
existing := make(map[string]partition)
expectedAndExists := make(map[string]bool)
for _, t := range existingTables {
existing[t.PartitionName()] = t
}
for _, t := range expectedTables {
if _, found := existing[t.PartitionName()]; found {
expectedAndExists[t.PartitionName()] = true
} else {
missingTables = append(missingTables, t)
}
}
for _, t := range existingTables {
if _, found := expectedAndExists[t.PartitionName()]; !found {
// Only in existingTables and not in both
unexpectedTables = append(unexpectedTables, t)
}
}
return unexpectedTables, missingTables
}
func (pm *partman) existingPartitions(parts []string) ([]partition, error) {
existing := make([]partition, 0, len(parts))
for _, v := range parts {
p, err := parsePartName(v)
2024-11-28 10:11:46 -05:00
if err != nil {
2024-11-29 19:22:50 -05:00
return nil, err
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
existing = append(existing, p)
}
return existing, nil
}
func (pm *partman) fullTableName(s string) string {
return fmt.Sprintf("%s.%s", pm.cfg.Schema, s)
}
func (pm *partman) prunePartition(ctx context.Context, tx database.Store, p partition) error {
s, e := p.Range()
start := pgtype.Timestamptz{Time: s, Valid: true}
end := pgtype.Timestamptz{Time: e, Valid: true}
err := tx.SweepCalls(ctx, start, end)
if err != nil {
return err
}
err = tx.CleanupSweptCalls(ctx, start, end)
if err != nil {
return err
}
err = tx.DetachPartition(ctx, pm.fullTableName(p.PartitionName()))
if err != nil {
return err
}
if pm.cfg.Drop {
return tx.DropPartition(ctx, pm.fullTableName(p.PartitionName()))
}
return nil
}
func (pm *partman) Check(ctx context.Context, now time.Time) error {
return pm.db.InTx(ctx, func(db database.Store) error {
// by default, we want to make sure a partition exists for this and next month
// since we run this at startup, it's safe to do only that.
partitions, err := db.GetTablePartitions(ctx, pm.cfg.Schema, callsTable)
2024-11-28 10:11:46 -05:00
if err != nil {
return err
}
2024-11-29 19:22:50 -05:00
existing, err := pm.existingPartitions(partitions)
2024-11-28 10:11:46 -05:00
if err != nil {
return err
}
2024-11-29 19:22:50 -05:00
expected := pm.expectedPartitions(now)
2024-11-28 10:11:46 -05:00
2024-11-29 19:22:50 -05:00
unexpected, missing := pm.comparePartitions(existing, expected)
2024-11-28 10:11:46 -05:00
2024-11-29 19:22:50 -05:00
for _, p := range unexpected {
err := pm.prunePartition(ctx, db, p)
if err != nil {
return err
}
}
2024-11-28 10:11:46 -05:00
2024-11-29 19:22:50 -05:00
for _, p := range missing {
err := pm.createPartition(ctx, db, p)
if err != nil {
return err
2024-11-28 10:11:46 -05:00
}
}
return nil
}, pgx.TxOptions{})
2024-11-29 19:22:50 -05:00
}
2024-11-28 10:11:46 -05:00
2024-11-29 19:22:50 -05:00
func (pm *partman) createPartition(ctx context.Context, tx database.Store, part partition) error {
start, end := part.Range()
return tx.CreatePartition(ctx, callsTable, part.PartitionName(), start, end)
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func parsePartName(p string) (partition, error) {
dateAr := strings.Split(p, "calls_p_")
2024-11-28 10:11:46 -05:00
if len(dateAr) != 2 {
2024-11-29 19:22:50 -05:00
return nil, PartitionError(p)
2024-11-28 10:11:46 -05:00
}
dateAr = strings.Split(dateAr[1], "_")
if len(dateAr) != 2 {
2024-11-29 19:22:50 -05:00
return nil, PartitionError(p)
2024-11-28 10:11:46 -05:00
}
year, err := strconv.Atoi(dateAr[0])
if err != nil {
2024-11-29 19:22:50 -05:00
return nil, PartitionError(p)
2024-11-28 10:11:46 -05:00
}
month, err := strconv.Atoi(dateAr[1])
if err != nil {
2024-11-29 19:22:50 -05:00
return nil, PartitionError(p)
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
r := monthlyPartition{time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)}
2024-11-28 10:11:46 -05:00
return r, nil
}
type partMap map[partition]struct{}
func (pm partMap) exists(dt partition) bool {
2024-11-29 19:22:50 -05:00
_, ex := pm[dt]
2024-11-28 10:11:46 -05:00
return ex
}
2024-11-29 19:22:50 -05:00
func partitionsMap(partitions []string, mustExist map[partition]struct{}) (partMap, error) {
2024-11-28 10:11:46 -05:00
partsDate := make(partMap, len(partitions))
for _, p := range partitions {
dt, err := parsePartName(p)
if err != nil {
return nil, err
}
partsDate[dt] = struct{}{}
}
return partsDate, nil
}
2024-11-29 19:22:50 -05:00
func runRetention() {
// make sure to check if partition was attached first
// before dropping. don't want to accidentally drop pre-detached partitions.
2024-11-28 10:11:46 -05:00
}
2024-11-29 19:22:50 -05:00
func dropPart() {
// intx
// SweepCalls
// DropPart
2024-11-28 10:11:46 -05:00
}