ws and pruner done!!!
This commit is contained in:
parent
c5cc245e25
commit
74add408e6
34 changed files with 1455 additions and 221 deletions
23
store/env_test.go
Normal file
23
store/env_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/genudine/saerro-go/util/testutil"
|
||||
)
|
||||
|
||||
func mkEnv(t *testing.T) (context.Context, *sql.DB) {
|
||||
t.Helper()
|
||||
|
||||
db := testutil.GetTestDB(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
})
|
||||
|
||||
return ctx, db
|
||||
}
|
123
store/player.go
123
store/player.go
|
@ -5,10 +5,26 @@ import (
|
|||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/genudine/saerro-go/types"
|
||||
"github.com/genudine/saerro-go/util"
|
||||
|
||||
"github.com/avast/retry-go"
|
||||
)
|
||||
|
||||
type IPlayerStore interface {
|
||||
IsMigrated(context.Context) bool
|
||||
RunMigration(context.Context, bool)
|
||||
Insert(context.Context, *types.Player) error
|
||||
GetOne(context.Context, string) (*types.Player, error)
|
||||
Prune(context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type PlayerStore struct {
|
||||
DB *sql.DB
|
||||
|
||||
// Test introspection for if migrations ran during this PlayerStore init
|
||||
RanMigration bool
|
||||
}
|
||||
|
||||
func NewPlayerStore(db *sql.DB) *PlayerStore {
|
||||
|
@ -24,10 +40,19 @@ func NewPlayerStore(db *sql.DB) *PlayerStore {
|
|||
return ps
|
||||
}
|
||||
|
||||
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||
if !force {
|
||||
// check if migrated first...
|
||||
func (ps *PlayerStore) IsMigrated(ctx context.Context) bool {
|
||||
_, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM players LIMIT 1;`)
|
||||
if err != nil {
|
||||
log.Printf("IsMigrated check failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||
if !force && ps.IsMigrated(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("(Re)-creating players table")
|
||||
|
@ -36,13 +61,101 @@ func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
|||
|
||||
CREATE TABLE players (
|
||||
character_id TEXT NOT NULL PRIMARY KEY,
|
||||
last_updated TIMESTAMPTZ NOT NULL,
|
||||
last_updated TIMESTAMP NOT NULL,
|
||||
world_id INT NOT NULL,
|
||||
faction_id INT NOT NULL,
|
||||
zone_id INT NOT NULL,
|
||||
class_name TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
-- TODO: Add indexes?
|
||||
`)
|
||||
log.Println("Done, players table is initialized.")
|
||||
ps.RanMigration = true
|
||||
}
|
||||
|
||||
// Insert a player into the store.
|
||||
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
|
||||
func (ps *PlayerStore) Insert(ctx context.Context, player *types.Player) error {
|
||||
if player.LastUpdated.IsZero() {
|
||||
player.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
err := retry.Do(func() error {
|
||||
_, err := ps.DB.ExecContext(ctx,
|
||||
`
|
||||
INSERT INTO players (
|
||||
last_updated, character_id, world_id, faction_id, zone_id, class_name
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (character_id) DO UPDATE SET
|
||||
last_updated = EXCLUDED.last_updated,
|
||||
world_id = EXCLUDED.world_id,
|
||||
faction_id = EXCLUDED.faction_id,
|
||||
zone_id = EXCLUDED.zone_id,
|
||||
class_name = EXCLUDED.class_name;
|
||||
`,
|
||||
util.TimeToString(player.LastUpdated),
|
||||
player.CharacterID,
|
||||
player.WorldID,
|
||||
player.FactionID,
|
||||
player.ZoneID,
|
||||
player.ClassName,
|
||||
)
|
||||
|
||||
return err
|
||||
}, retry.Attempts(2))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOne player from the store.
|
||||
func (ps *PlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
|
||||
row := ps.DB.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
last_updated,
|
||||
character_id,
|
||||
world_id,
|
||||
faction_id,
|
||||
zone_id,
|
||||
class_name
|
||||
FROM players WHERE character_id = $1
|
||||
`, id)
|
||||
|
||||
player := &types.Player{}
|
||||
var timestamp string
|
||||
|
||||
err := row.Scan(
|
||||
×tamp,
|
||||
&player.CharacterID,
|
||||
&player.WorldID,
|
||||
&player.FactionID,
|
||||
&player.ZoneID,
|
||||
&player.ClassName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
player.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
|
||||
|
||||
return player, err
|
||||
}
|
||||
|
||||
func (ps *PlayerStore) Prune(ctx context.Context) (int64, error) {
|
||||
log.Println("pruning PlayerStore")
|
||||
|
||||
// Avoid using sql idioms here for portability
|
||||
// SQLite and PgSQL do now() differently, we don't need to at all.
|
||||
res, err := ps.DB.ExecContext(ctx,
|
||||
`
|
||||
DELETE FROM players WHERE last_updated < $1;
|
||||
`,
|
||||
util.TimeToString(time.Now().Add(-time.Minute*15)),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
|
113
store/player_test.go
Normal file
113
store/player_test.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/genudine/saerro-go/store"
|
||||
"github.com/genudine/saerro-go/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlayerMigrationClean(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps := store.NewPlayerStore(db)
|
||||
|
||||
isMigrated := ps.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
}
|
||||
|
||||
func TestPlayerMultipleStartups(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps1 := store.NewPlayerStore(db)
|
||||
ps2 := store.NewPlayerStore(db)
|
||||
|
||||
isMigrated := ps2.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
|
||||
assert.True(t, ps1.RanMigration)
|
||||
assert.False(t, ps2.RanMigration)
|
||||
}
|
||||
|
||||
func TestPlayerMigrationRerun(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps := store.NewPlayerStore(db)
|
||||
ps.RunMigration(ctx, true)
|
||||
|
||||
isMigrated := ps.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
}
|
||||
|
||||
func TestPlayerInsertGetOne(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
ps := store.NewPlayerStore(db)
|
||||
|
||||
player := &types.Player{
|
||||
CharacterID: "41666",
|
||||
WorldID: 17,
|
||||
FactionID: types.TR,
|
||||
ZoneID: 4,
|
||||
ClassName: "light_assault",
|
||||
}
|
||||
|
||||
err := ps.Insert(ctx, player)
|
||||
assert.NoError(t, err, "Insert failed")
|
||||
|
||||
p1, err := ps.GetOne(ctx, "41666")
|
||||
assert.NoError(t, err, "GetOne failed")
|
||||
assert.Equal(t, "light_assault", p1.ClassName)
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
player.ClassName = "combat_medic"
|
||||
player.LastUpdated = time.Time{}
|
||||
err = ps.Insert(ctx, player)
|
||||
assert.NoError(t, err, "Insert failed")
|
||||
|
||||
p2, err := ps.GetOne(ctx, "41666")
|
||||
assert.NoError(t, err, "GetOne failed")
|
||||
assert.Equal(t, "combat_medic", p2.ClassName)
|
||||
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
|
||||
}
|
||||
|
||||
func TestPlayerPrune(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
ps := store.NewPlayerStore(db)
|
||||
|
||||
prunedPlayer := &types.Player{
|
||||
CharacterID: "20155",
|
||||
WorldID: 17,
|
||||
FactionID: types.NC,
|
||||
ZoneID: 4,
|
||||
ClassName: "light_assault",
|
||||
LastUpdated: time.Now().Add(-time.Minute * 20),
|
||||
}
|
||||
survivingPlayer := &types.Player{
|
||||
CharacterID: "41666",
|
||||
WorldID: 17,
|
||||
FactionID: types.TR,
|
||||
ZoneID: 4,
|
||||
ClassName: "light_assault",
|
||||
LastUpdated: time.Now().Add(-time.Minute * 5),
|
||||
}
|
||||
|
||||
err := ps.Insert(ctx, prunedPlayer)
|
||||
assert.NoError(t, err, "Insert prunedPlayer failed")
|
||||
|
||||
err = ps.Insert(ctx, survivingPlayer)
|
||||
assert.NoError(t, err, "Insert survivingPlayer failed")
|
||||
|
||||
removed, err := ps.Prune(ctx)
|
||||
assert.NoError(t, err, "Prune failed")
|
||||
assert.Equal(t, int64(1), removed, "Prune count incorrect")
|
||||
|
||||
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
|
||||
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
|
||||
|
||||
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
|
||||
assert.NoError(t, err, "GetOne survivingPlayer failed")
|
||||
}
|
45
store/storemock/playerstore.go
Normal file
45
store/storemock/playerstore.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package storemock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/genudine/saerro-go/types"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockPlayerStore struct {
|
||||
mock.Mock
|
||||
|
||||
DB *sql.DB
|
||||
RanMigration bool
|
||||
}
|
||||
|
||||
func (m *MockPlayerStore) IsMigrated(ctx context.Context) bool {
|
||||
args := m.Called(ctx)
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
func (m *MockPlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||
m.Called(ctx, force)
|
||||
}
|
||||
|
||||
func (m *MockPlayerStore) Insert(ctx context.Context, player *types.Player) error {
|
||||
args := m.Called(ctx, player)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockPlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
|
||||
args := m.Called(ctx, id)
|
||||
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return args.Get(0).(*types.Player), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockPlayerStore) Prune(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
149
store/vehicle.go
Normal file
149
store/vehicle.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/genudine/saerro-go/types"
|
||||
|
||||
"github.com/avast/retry-go"
|
||||
)
|
||||
|
||||
type VehicleStore struct {
|
||||
DB *sql.DB
|
||||
|
||||
// Test introspection for if migrations ran during this PlayerStore init
|
||||
RanMigration bool
|
||||
}
|
||||
|
||||
func NewVehicleStore(db *sql.DB) *VehicleStore {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
ps := &VehicleStore{
|
||||
DB: db,
|
||||
}
|
||||
|
||||
ps.RunMigration(ctx, false)
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
func (ps *VehicleStore) IsMigrated(ctx context.Context) bool {
|
||||
_, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM vehicles LIMIT 1;`)
|
||||
if err != nil {
|
||||
log.Printf("IsMigrated check failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ps *VehicleStore) RunMigration(ctx context.Context, force bool) {
|
||||
if !force && ps.IsMigrated(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("(Re)-creating vehicles table")
|
||||
ps.DB.ExecContext(ctx, `
|
||||
DROP TABLE IF EXISTS vehicles;
|
||||
|
||||
CREATE TABLE vehicles (
|
||||
character_id TEXT NOT NULL PRIMARY KEY,
|
||||
last_updated TIMESTAMP NOT NULL,
|
||||
world_id INT NOT NULL,
|
||||
faction_id INT NOT NULL,
|
||||
zone_id INT NOT NULL,
|
||||
vehicle_name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- TODO: Add indexes?
|
||||
`)
|
||||
log.Println("Done, vehicles table is initialized.")
|
||||
ps.RanMigration = true
|
||||
}
|
||||
|
||||
// Insert a player into the store.
|
||||
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
|
||||
func (ps *VehicleStore) Insert(ctx context.Context, vehicle *types.Vehicle) error {
|
||||
if vehicle.LastUpdated.IsZero() {
|
||||
vehicle.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
err := retry.Do(func() error {
|
||||
_, err := ps.DB.ExecContext(ctx,
|
||||
`
|
||||
INSERT INTO vehicles (
|
||||
last_updated, character_id, world_id, faction_id, zone_id, vehicle_name
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (character_id) DO UPDATE SET
|
||||
last_updated = EXCLUDED.last_updated,
|
||||
world_id = EXCLUDED.world_id,
|
||||
faction_id = EXCLUDED.faction_id,
|
||||
zone_id = EXCLUDED.zone_id,
|
||||
vehicle_name = EXCLUDED.vehicle_name
|
||||
`,
|
||||
vehicle.LastUpdated,
|
||||
vehicle.CharacterID,
|
||||
vehicle.WorldID,
|
||||
vehicle.FactionID,
|
||||
vehicle.ZoneID,
|
||||
vehicle.VehicleName,
|
||||
)
|
||||
|
||||
return err
|
||||
}, retry.Attempts(2))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOne player from the store.
|
||||
func (ps *VehicleStore) GetOne(ctx context.Context, id string) (*types.Vehicle, error) {
|
||||
row := ps.DB.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
last_updated,
|
||||
character_id,
|
||||
world_id,
|
||||
faction_id,
|
||||
zone_id,
|
||||
vehicle_name
|
||||
FROM vehicles WHERE character_id = $1
|
||||
`, id)
|
||||
|
||||
vehicle := &types.Vehicle{}
|
||||
var timestamp string
|
||||
|
||||
err := row.Scan(
|
||||
×tamp,
|
||||
&vehicle.CharacterID,
|
||||
&vehicle.WorldID,
|
||||
&vehicle.FactionID,
|
||||
&vehicle.ZoneID,
|
||||
&vehicle.VehicleName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vehicle.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
|
||||
|
||||
return vehicle, err
|
||||
}
|
||||
|
||||
func (ps *VehicleStore) Prune(ctx context.Context) (int64, error) {
|
||||
log.Println("pruning VehicleStore")
|
||||
|
||||
// Avoid using sql idioms here for portability
|
||||
// SQLite and PgSQL do now() differently, we don't need to at all.
|
||||
res, err := ps.DB.ExecContext(ctx, `
|
||||
DELETE FROM vehicles WHERE last_updated < $1;
|
||||
`, time.Now().Add(-time.Minute*15))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
114
store/vehicle_test.go
Normal file
114
store/vehicle_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/genudine/saerro-go/store"
|
||||
"github.com/genudine/saerro-go/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVehicleMigrationClean(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps := store.NewVehicleStore(db)
|
||||
|
||||
isMigrated := ps.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
}
|
||||
|
||||
func TestVehicleMultipleStartups(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps1 := store.NewVehicleStore(db)
|
||||
ps2 := store.NewVehicleStore(db)
|
||||
|
||||
isMigrated := ps2.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
|
||||
assert.True(t, ps1.RanMigration)
|
||||
assert.False(t, ps2.RanMigration)
|
||||
}
|
||||
|
||||
func TestVehicleMigrationRerun(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
|
||||
ps := store.NewVehicleStore(db)
|
||||
ps.RunMigration(ctx, true)
|
||||
|
||||
isMigrated := ps.IsMigrated(ctx)
|
||||
assert.True(t, isMigrated)
|
||||
}
|
||||
|
||||
func TestVehicleInsertGetOne(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
ps := store.NewVehicleStore(db)
|
||||
|
||||
player := &types.Vehicle{
|
||||
CharacterID: "41666",
|
||||
WorldID: 17,
|
||||
FactionID: types.TR,
|
||||
ZoneID: 4,
|
||||
VehicleName: "harasser",
|
||||
}
|
||||
|
||||
err := ps.Insert(ctx, player)
|
||||
assert.NoError(t, err, "Insert failed")
|
||||
|
||||
p1, err := ps.GetOne(ctx, "41666")
|
||||
assert.NoError(t, err, "GetOne failed")
|
||||
assert.Equal(t, "harasser", p1.VehicleName)
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
player.VehicleName = "vanguard"
|
||||
player.LastUpdated = time.Time{}
|
||||
err = ps.Insert(ctx, player)
|
||||
assert.NoError(t, err, "Insert failed")
|
||||
|
||||
p2, err := ps.GetOne(ctx, "41666")
|
||||
assert.NoError(t, err, "GetOne failed")
|
||||
assert.Equal(t, "vanguard", p2.VehicleName)
|
||||
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
|
||||
}
|
||||
|
||||
func TestVehiclePrune(t *testing.T) {
|
||||
ctx, db := mkEnv(t)
|
||||
ps := store.NewVehicleStore(db)
|
||||
|
||||
prunedPlayer := &types.Vehicle{
|
||||
CharacterID: "20155",
|
||||
WorldID: 17,
|
||||
FactionID: types.NC,
|
||||
ZoneID: 4,
|
||||
VehicleName: "harasser",
|
||||
|
||||
LastUpdated: time.Now().Add(-time.Minute * 20),
|
||||
}
|
||||
survivingPlayer := &types.Vehicle{
|
||||
CharacterID: "41666",
|
||||
WorldID: 17,
|
||||
FactionID: types.TR,
|
||||
ZoneID: 4,
|
||||
VehicleName: "harasser",
|
||||
LastUpdated: time.Now().Add(-time.Minute * 5),
|
||||
}
|
||||
|
||||
err := ps.Insert(ctx, prunedPlayer)
|
||||
assert.NoError(t, err, "Insert prunedPlayer failed")
|
||||
|
||||
err = ps.Insert(ctx, survivingPlayer)
|
||||
assert.NoError(t, err, "Insert survivingPlayer failed")
|
||||
|
||||
removed, err := ps.Prune(ctx)
|
||||
assert.NoError(t, err, "Prune failed")
|
||||
assert.Equal(t, int64(1), removed, "Prune count incorrect")
|
||||
|
||||
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
|
||||
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
|
||||
|
||||
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
|
||||
assert.NoError(t, err, "GetOne survivingPlayer failed")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue