update state history session type
All checks were successful
Release and Deploy / build (push) Successful in 2m25s
Release and Deploy / deploy (push) Successful in 26s

This commit is contained in:
Fran Jurmanović
2025-09-15 19:11:25 +02:00
parent 4ab94de529
commit 760412d7db
13 changed files with 386 additions and 205 deletions

View File

@@ -0,0 +1,124 @@
package migrations
import (
"acc-server-manager/local/utl/logging"
"fmt"
"gorm.io/gorm"
)
// UpdateStateHistorySessions migrates tables from integer IDs to UUIDs
type UpdateStateHistorySessions struct {
DB *gorm.DB
}
// NewUpdateStateHistorySessions creates a new UUID migration
func NewUpdateStateHistorySessions(db *gorm.DB) *UpdateStateHistorySessions {
return &UpdateStateHistorySessions{DB: db}
}
// Up executes the migration
func (m *UpdateStateHistorySessions) Up() error {
logging.Info("Checking UUID migration...")
// Check if migration is needed by looking at the servers table structure
if !m.needsMigration() {
logging.Info("UUID migration not needed - tables already use UUID primary keys")
return nil
}
logging.Info("Starting UUID migration...")
// Check if migration has already been applied
var migrationRecord MigrationRecord
err := m.DB.Where("migration_name = ?", "002_migrate_to_uuid").First(&migrationRecord).Error
if err == nil {
logging.Info("UUID migration already applied, skipping")
return nil
}
// Create migration tracking table if it doesn't exist
if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil {
return fmt.Errorf("failed to create migration tracking table: %v", err)
}
// Execute the UUID migration using the existing migration function
logging.Info("Executing UUID migration...")
if err := runUUIDMigrationSQL(m.DB); err != nil {
return fmt.Errorf("failed to execute UUID migration: %v", err)
}
logging.Info("UUID migration completed successfully")
return nil
}
// needsMigration checks if the UUID migration is needed by examining table structure
func (m *UpdateStateHistorySessions) needsMigration() bool {
// Check if servers table exists and has integer primary key
var result struct {
Exists bool `gorm:"column:exists"`
}
err := m.DB.Raw(`
SELECT count(*) > 0 as exists FROM state_history
WHERE length(session) > 1 LIMIT 1;
`).Scan(&result).Error
if err != nil || !result.Exists {
// Table doesn't exist or no primary key found - assume no migration needed
return false
}
return result.Exists
}
// Down reverses the migration (not implemented for safety)
func (m *UpdateStateHistorySessions) Down() error {
logging.Error("UUID migration rollback is not supported for data safety reasons")
return fmt.Errorf("UUID migration rollback is not supported")
}
// runUpdateStateHistorySessionsMigration executes the UUID migration using the SQL file
func runUpdateStateHistorySessionsMigration(db *gorm.DB) error {
// Disable foreign key constraints during migration
if err := db.Exec("PRAGMA foreign_keys=OFF").Error; err != nil {
return fmt.Errorf("failed to disable foreign keys: %v", err)
}
// Start transaction
tx := db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to start transaction: %v", tx.Error)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
migrationSQL := "UPDATE state_history SET session = upper(substr(session, 1, 1));"
// Execute the migration
if err := tx.Exec(string(migrationSQL)).Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute migration: %v", err)
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit migration: %v", err)
}
// Re-enable foreign key constraints
if err := db.Exec("PRAGMA foreign_keys=ON").Error; err != nil {
return fmt.Errorf("failed to re-enable foreign keys: %v", err)
}
return nil
}
// RunUpdateStateHistorySessionsMigration is a convenience function to run the migration
func RunUpdateStateHistorySessionsMigration(db *gorm.DB) error {
migration := NewUpdateStateHistorySessions(db)
return migration.Up()
}

View File

@@ -79,11 +79,11 @@ type EventConfig struct {
}
type Session struct {
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType string `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType TrackSession `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
}
type AssistRules struct {

View File

@@ -73,12 +73,12 @@ type State struct {
type ServerState struct {
sync.RWMutex `swaggerignore:"-" json:"-"`
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
Session TrackSession `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState
// etc.
}

View File

@@ -1,6 +1,9 @@
package model
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/uuid"
@@ -13,9 +16,9 @@ type StateHistoryFilter struct {
DateRangeFilter // Adds date range filtering
// Additional fields specific to state history
Session string `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
Session TrackSession `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
}
// ApplyFilter implements the Filterable interface
@@ -52,16 +55,66 @@ func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
return query
}
type TrackSession string
const (
SessionPractice TrackSession = "P"
SessionQualify TrackSession = "Q"
SessionRace TrackSession = "R"
SessionUnknown TrackSession = "U"
)
func (i *TrackSession) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err == nil {
*i = ToTrackSession(str)
return nil
}
return fmt.Errorf("invalid TrackSession value")
}
func (i TrackSession) Humanize() string {
switch i {
case SessionPractice:
return "Practice"
case SessionQualify:
return "Qualifying"
case SessionRace:
return "Race"
default:
return "Unknown"
}
}
func ToTrackSession(i string) TrackSession {
sessionAbrv := strings.ToUpper(i[:1])
switch sessionAbrv {
case "P":
return SessionPractice
case "Q":
return SessionQualify
case "R":
return SessionRace
default:
return SessionUnknown
}
}
func (i TrackSession) ToString() string {
return string(i)
}
type StateHistory struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"` // Unique identifier for each session/event
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
Session TrackSession `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"` // Unique identifier for each session/event
}
// BeforeCreate is a GORM hook that runs before creating new state history entries

View File

@@ -3,8 +3,8 @@ package model
import "github.com/google/uuid"
type SessionCount struct {
Name string `json:"name"`
Count int `json:"count"`
Name TrackSession `json:"name"`
Count int `json:"count"`
}
type DailyActivity struct {
@@ -29,10 +29,10 @@ type StateHistoryStats struct {
}
type RecentSession struct {
ID uuid.UUID `json:"id"`
Date string `json:"date"`
Type string `json:"type"`
Track string `json:"track"`
Duration int `json:"duration"`
Players int `json:"players"`
ID uuid.UUID `json:"id"`
Date string `json:"date"`
Type TrackSession `json:"type"`
Track string `json:"track"`
Duration int `json:"duration"`
Players int `json:"players"`
}

View File

@@ -159,7 +159,7 @@ func (s *ServerService) insertStateHistory(serverID uuid.UUID, state *model.Serv
})
}
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) {
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType model.TrackSession) {
// Get configs using helper methods
event, err := s.configService.GetEventConfig(server)
if err != nil {

View File

@@ -11,102 +11,104 @@ import (
)
type StateChange int
const (
PlayerCount StateChange = iota
Session
PlayerCount StateChange = iota
Session
)
var StateChanges = map[StateChange]string {
PlayerCount: "player-count",
Session: "session",
var StateChanges = map[StateChange]string{
PlayerCount: "player-count",
Session: "session",
}
type AccServerInstance struct {
Model *model.Server
State *model.ServerState
OnStateChange func(*model.ServerState, ...StateChange)
Model *model.Server
State *model.ServerState
OnStateChange func(*model.ServerState, ...StateChange)
}
func NewAccServerInstance(server *model.Server, onStateChange func(*model.ServerState, ...StateChange)) *AccServerInstance {
return &AccServerInstance{
Model: server,
State: &model.ServerState{PlayerCount: 0},
OnStateChange: onStateChange,
}
return &AccServerInstance{
Model: server,
State: &model.ServerState{PlayerCount: 0},
OnStateChange: onStateChange,
}
}
type StateRegexHandler struct {
*regex_handler.RegexHandler
test string
*regex_handler.RegexHandler
test string
}
func NewRegexHandler(str string, test string) *StateRegexHandler {
return &StateRegexHandler{
RegexHandler: regex_handler.New(str),
test: test,
RegexHandler: regex_handler.New(str),
test: test,
}
}
func (rh *StateRegexHandler) Test(line string) bool{
return strings.Contains(line, rh.test)
func (rh *StateRegexHandler) Test(line string) bool {
return strings.Contains(line, rh.test)
}
func (rh *StateRegexHandler) Count(line string) int{
var count int = 0
rh.Contains(line, func (strs ...string) {
if len(strs) == 2 {
if ct, err := strconv.Atoi(strs[1]); err == nil {
count = ct
}
}
})
return count
func (rh *StateRegexHandler) Count(line string) int {
var count int = 0
rh.Contains(line, func(strs ...string) {
if len(strs) == 2 {
if ct, err := strconv.Atoi(strs[1]); err == nil {
count = ct
}
}
})
return count
}
func (rh *StateRegexHandler) Change(line string) (string, string){
var old string = ""
var new string = ""
rh.Contains(line, func (strs ...string) {
if len(strs) == 3 {
old = strs[1]
new = strs[2]
}
})
return old, new
func (rh *StateRegexHandler) Change(line string) (string, string) {
var old string = ""
var new string = ""
rh.Contains(line, func(strs ...string) {
if len(strs) == 3 {
old = strs[1]
new = strs[2]
}
})
return old, new
}
func TailLogFile(path string, callback func(string)) {
file, _ := os.Open(path)
defer file.Close()
file, _ := os.Open(path)
defer file.Close()
file.Seek(0, os.SEEK_END) // Start at end of file
reader := bufio.NewReader(file)
file.Seek(0, os.SEEK_END) // Start at end of file
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err == nil {
callback(line)
} else {
time.Sleep(500 * time.Millisecond) // wait for new data
}
}
for {
line, err := reader.ReadString('\n')
if err == nil {
callback(line)
} else {
time.Sleep(500 * time.Millisecond) // wait for new data
}
}
}
type LogStateType int
const (
SessionChange LogStateType = iota
LeaderboardUpdate
UDPCount
ClientsOnline
RemovingDeadConnection
SessionChange LogStateType = iota
LeaderboardUpdate
UDPCount
ClientsOnline
RemovingDeadConnection
)
var logStateContain = map[LogStateType]string {
SessionChange: "Session changed",
LeaderboardUpdate: "Updated leaderboard for",
UDPCount: "Udp message count",
ClientsOnline: "client(s) online",
RemovingDeadConnection: "Removing dead connection",
var logStateContain = map[LogStateType]string{
SessionChange: "Session changed",
LeaderboardUpdate: "Updated leaderboard for",
UDPCount: "Udp message count",
ClientsOnline: "client(s) online",
RemovingDeadConnection: "Removing dead connection",
}
var sessionChangeRegex = NewRegexHandler(`Session changed: (\w+) -> (\w+)`, logStateContain[SessionChange])
@@ -115,75 +117,77 @@ var udpCountRegex = NewRegexHandler(`Udp message count (\d+) client`, logStateCo
var clientsOnlineRegex = NewRegexHandler(`(\d+) client\(s\) online`, logStateContain[ClientsOnline])
var removingDeadConnectionsRegex = NewRegexHandler(`Removing dead connection`, logStateContain[RemovingDeadConnection])
var logStateRegex = map[LogStateType]*StateRegexHandler {
SessionChange: sessionChangeRegex,
LeaderboardUpdate: leaderboardUpdateRegex,
UDPCount: udpCountRegex,
ClientsOnline: clientsOnlineRegex,
RemovingDeadConnection: removingDeadConnectionsRegex,
var logStateRegex = map[LogStateType]*StateRegexHandler{
SessionChange: sessionChangeRegex,
LeaderboardUpdate: leaderboardUpdateRegex,
UDPCount: udpCountRegex,
ClientsOnline: clientsOnlineRegex,
RemovingDeadConnection: removingDeadConnectionsRegex,
}
func (instance *AccServerInstance) HandleLogLine(line string) {
for logState, regexHandler := range logStateRegex {
if (regexHandler.Test(line)) {
switch logState {
case LeaderboardUpdate:
case UDPCount:
case ClientsOnline:
count := regexHandler.Count(line)
instance.UpdatePlayerCount(count)
case SessionChange:
_, new := regexHandler.Change(line)
instance.UpdateSessionChange(new)
case RemovingDeadConnection:
instance.UpdatePlayerCount(instance.State.PlayerCount - 1)
}
}
}
for logState, regexHandler := range logStateRegex {
if regexHandler.Test(line) {
switch logState {
case LeaderboardUpdate:
case UDPCount:
case ClientsOnline:
count := regexHandler.Count(line)
instance.UpdatePlayerCount(count)
case SessionChange:
_, new := regexHandler.Change(line)
trackSession := model.ToTrackSession(new)
instance.UpdateSessionChange(trackSession)
case RemovingDeadConnection:
instance.UpdatePlayerCount(instance.State.PlayerCount - 1)
}
}
}
}
func (instance *AccServerInstance) UpdateState(callback func(state *model.ServerState, changes *[]StateChange)) {
state := instance.State
changes := []StateChange{}
state.Lock()
defer state.Unlock()
callback(state, &changes)
if (len(changes) > 0) {
instance.OnStateChange(state, changes...)
}
state := instance.State
changes := []StateChange{}
state.Lock()
defer state.Unlock()
callback(state, &changes)
if len(changes) > 0 {
instance.OnStateChange(state, changes...)
}
}
func (instance *AccServerInstance) UpdatePlayerCount(count int) {
if (count < 0) {
return
}
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
if (count == state.PlayerCount) {
return
}
if (count > 0 && state.PlayerCount == 0) {
state.SessionStart = time.Now()
*changes = append(*changes, Session)
} else if (count == 0) {
state.SessionStart = time.Time{}
*changes = append(*changes, Session)
}
state.PlayerCount = count
*changes = append(*changes, PlayerCount)
})
if count < 0 {
return
}
instance.UpdateState(func(state *model.ServerState, changes *[]StateChange) {
if count == state.PlayerCount {
return
}
if count > 0 && state.PlayerCount == 0 {
state.SessionStart = time.Now()
*changes = append(*changes, Session)
} else if count == 0 {
state.SessionStart = time.Time{}
*changes = append(*changes, Session)
}
state.PlayerCount = count
*changes = append(*changes, PlayerCount)
})
}
func (instance *AccServerInstance) UpdateSessionChange(session string) {
instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) {
if (session == state.Session) {
return
}
if (state.PlayerCount > 0) {
state.SessionStart = time.Now()
} else {
state.SessionStart = time.Time{}
}
state.Session = session
*changes = append(*changes, Session)
})
}
func (instance *AccServerInstance) UpdateSessionChange(session model.TrackSession) {
instance.UpdateState(func(state *model.ServerState, changes *[]StateChange) {
if session == state.Session {
return
}
if state.PlayerCount > 0 {
state.SessionStart = time.Now()
} else {
state.SessionStart = time.Time{}
}
state.Session = session
*changes = append(*changes, Session)
})
}