639 lines
18 KiB
Go
639 lines
18 KiB
Go
//go:build ignore
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/common"
|
|
"chatapp3-golang/internal/config"
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/service/wheel"
|
|
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
const (
|
|
simSysOrigin = "LOCAL_WHEEL_RANDOM_SIM"
|
|
simRoomID = int64(9002001)
|
|
simUserIDBase = int64(2042274349300000000)
|
|
probabilityTotal = 1000000
|
|
targetRewardCount = 100000
|
|
|
|
receiptIncome = "INCOME"
|
|
receiptExpense = "EXPENDITURE"
|
|
)
|
|
|
|
type categorySpec struct {
|
|
Category string
|
|
RewardCount int
|
|
PriceOneGold int64
|
|
PriceTenGold int64
|
|
PriceFiftyGold int64
|
|
GoldBase int64
|
|
}
|
|
|
|
type simGateway struct {
|
|
mu sync.Mutex
|
|
events map[string]integration.GoldReceiptCommand
|
|
changes []integration.GoldReceiptCommand
|
|
balance map[int64]int64
|
|
}
|
|
|
|
type topRewardSummary struct {
|
|
Category string `json:"category"`
|
|
Sort int `json:"sort"`
|
|
RewardID int64 `json:"rewardId,string"`
|
|
GoldAmount int64 `json:"goldAmount"`
|
|
Probability int `json:"probability"`
|
|
TotalLimit int64 `json:"totalLimit,omitempty"`
|
|
UserLimit int64 `json:"userLimit,omitempty"`
|
|
IssuedCount int64 `json:"issuedCount"`
|
|
ActualCount int64 `json:"actualCount"`
|
|
}
|
|
|
|
type simSummary struct {
|
|
DSNHost string `json:"dsnHost"`
|
|
SysOrigin string `json:"sysOrigin"`
|
|
Seed int64 `json:"seed"`
|
|
TargetRewardItems int `json:"targetRewardItems"`
|
|
ActualRewardItems int64 `json:"actualRewardItems"`
|
|
DrawRequests int64 `json:"drawRequests"`
|
|
RandomUsers int `json:"randomUsers"`
|
|
UniqueUsersDrawn int `json:"uniqueUsersDrawn"`
|
|
DrawTimesRequests map[string]int64 `json:"drawTimesRequests"`
|
|
DrawTimesRewardItems map[string]int64 `json:"drawTimesRewardItems"`
|
|
CategoryRewardItems map[string]int64 `json:"categoryRewardItems"`
|
|
StatusCounts map[string]int64 `json:"statusCounts"`
|
|
PaymentEvents int `json:"paymentEvents"`
|
|
IncomeEvents int `json:"incomeEvents"`
|
|
LimitedRewards int `json:"limitedRewards"`
|
|
ExhaustedLimitedRewards int `json:"exhaustedLimitedRewards"`
|
|
TotalLimitViolations int `json:"totalLimitViolations"`
|
|
UserLimitViolations int `json:"userLimitViolations"`
|
|
IssuedCountMismatches int `json:"issuedCountMismatches"`
|
|
TopRewards []topRewardSummary `json:"topRewards"`
|
|
ElapsedMillis int64 `json:"elapsedMillis"`
|
|
}
|
|
|
|
func (g *simGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.events == nil {
|
|
g.events = map[string]integration.GoldReceiptCommand{}
|
|
}
|
|
if g.balance == nil {
|
|
g.balance = map[int64]int64{}
|
|
}
|
|
if _, exists := g.events[cmd.EventID]; exists {
|
|
return nil
|
|
}
|
|
switch strings.ToUpper(cmd.ReceiptType) {
|
|
case receiptExpense:
|
|
g.balance[cmd.UserID] -= cmd.Amount.DollarAmount
|
|
case receiptIncome:
|
|
g.balance[cmd.UserID] += cmd.Amount.DollarAmount
|
|
}
|
|
g.events[cmd.EventID] = cmd
|
|
g.changes = append(g.changes, cmd)
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) ExistsGoldEvent(_ context.Context, eventID string) (bool, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
_, exists := g.events[eventID]
|
|
return exists, nil
|
|
}
|
|
|
|
func (g *simGateway) GivePropsBackpack(context.Context, integration.GivePropsBackpackRequest) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) IncrGiftBackpack(context.Context, int64, int64, int) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) SwitchUseProps(context.Context, int64, int64) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) ActivateTemporaryBadge(context.Context, int64, int64, int) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) RemoveUserProfileCacheAll(context.Context, int64) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *simGateway) MapGoldBalance(_ context.Context, userIDs []int64) (map[int64]int64, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
result := make(map[int64]int64, len(userIDs))
|
|
for _, userID := range userIDs {
|
|
result[userID] = g.balance[userID]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (g *simGateway) GetRoomProfileByUserID(_ context.Context, userID int64) (integration.RoomProfile, error) {
|
|
return integration.RoomProfile{
|
|
ID: integration.Int64Value(simRoomID),
|
|
UserID: integration.Int64Value(userID),
|
|
RoomName: "local-wheel-random-sim-room",
|
|
SysOrigin: simSysOrigin,
|
|
}, nil
|
|
}
|
|
|
|
func (g *simGateway) countReceipt(receiptType string) int {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
count := 0
|
|
for _, change := range g.changes {
|
|
if strings.EqualFold(change.ReceiptType, receiptType) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
startedAt := time.Now()
|
|
seed := envInt64("WHEEL_SIM_SEED", time.Now().UnixNano())
|
|
target := envInt("WHEEL_SIM_TARGET_REWARDS", targetRewardCount)
|
|
userCount := envInt("WHEEL_SIM_USERS", 5000)
|
|
rng := rand.New(rand.NewSource(seed))
|
|
|
|
db, dsn, err := connectMySQL(ctx)
|
|
must(err, "connect mysql")
|
|
sqlDB, err := db.DB()
|
|
must(err, "unwrap mysql")
|
|
defer sqlDB.Close()
|
|
|
|
must(ensureWheelSchema(db), "ensure wheel schema")
|
|
must(cleanup(ctx, db, simSysOrigin), "cleanup previous sim data")
|
|
|
|
gateway := &simGateway{
|
|
events: map[string]integration.GoldReceiptCommand{},
|
|
balance: make(map[int64]int64, userCount),
|
|
}
|
|
for i := 0; i < userCount; i++ {
|
|
gateway.balance[simUserIDBase+int64(i)] = 1_000_000_000
|
|
}
|
|
service := wheel.NewService(config.Config{}, db, gateway)
|
|
must(seedRandomConfig(ctx, service, rng), "seed random wheel config")
|
|
|
|
categorySpecs := specs()
|
|
drawTimesRequests := map[string]int64{"1": 0, "10": 0, "50": 0}
|
|
drawTimesRewardItems := map[string]int64{"1": 0, "10": 0, "50": 0}
|
|
categoryRewardItems := map[string]int64{}
|
|
uniqueUsers := map[int64]struct{}{}
|
|
var drawRequests int64
|
|
var rewardItems int64
|
|
|
|
for rewardItems < int64(target) {
|
|
userID := simUserIDBase + int64(rng.Intn(userCount))
|
|
category := categorySpecs[rng.Intn(len(categorySpecs))].Category
|
|
times := randomDrawTimes(rng)
|
|
|
|
resp, err := service.Draw(ctx, common.AuthUser{UserID: userID, SysOrigin: simSysOrigin}, wheel.DrawRequest{
|
|
Category: category,
|
|
Times: times,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("draw failed after requests=%d rewards=%d user=%d category=%s times=%d: %v", drawRequests, rewardItems, userID, category, times, err)
|
|
}
|
|
if resp.GrantFailed {
|
|
log.Fatalf("grant failed after requests=%d rewards=%d drawNo=%s", drawRequests, rewardItems, resp.DrawNo)
|
|
}
|
|
|
|
drawRequests++
|
|
uniqueUsers[userID] = struct{}{}
|
|
key := strconv.Itoa(times)
|
|
drawTimesRequests[key]++
|
|
drawTimesRewardItems[key] += int64(len(resp.Records))
|
|
for _, record := range resp.Records {
|
|
rewardItems++
|
|
categoryRewardItems[record.Category]++
|
|
}
|
|
if rewardItems > 0 && rewardItems%20000 < int64(times) {
|
|
log.Printf("progress rewards=%d requests=%d", rewardItems, drawRequests)
|
|
}
|
|
}
|
|
|
|
statusCounts := loadStatusCounts(ctx, db)
|
|
topRewards := loadTopRewards(ctx, db, 12)
|
|
totalLimitViolations := countTotalLimitViolations(ctx, db)
|
|
userLimitViolations := countUserLimitViolations(ctx, db)
|
|
issuedCountMismatches := countIssuedCountMismatches(ctx, db)
|
|
limitedRewards, exhaustedLimitedRewards := countLimitedRewards(ctx, db)
|
|
|
|
assert(totalLimitViolations == 0, fmt.Sprintf("total limit violations = %d", totalLimitViolations))
|
|
assert(userLimitViolations == 0, fmt.Sprintf("user limit violations = %d", userLimitViolations))
|
|
assert(issuedCountMismatches == 0, fmt.Sprintf("issued count mismatches = %d", issuedCountMismatches))
|
|
assert(statusCounts["SUCCESS"] == rewardItems, fmt.Sprintf("success count = %d, rewards = %d", statusCounts["SUCCESS"], rewardItems))
|
|
|
|
summary := simSummary{
|
|
DSNHost: dsnLabel(dsn),
|
|
SysOrigin: simSysOrigin,
|
|
Seed: seed,
|
|
TargetRewardItems: target,
|
|
ActualRewardItems: rewardItems,
|
|
DrawRequests: drawRequests,
|
|
RandomUsers: userCount,
|
|
UniqueUsersDrawn: len(uniqueUsers),
|
|
DrawTimesRequests: drawTimesRequests,
|
|
DrawTimesRewardItems: drawTimesRewardItems,
|
|
CategoryRewardItems: categoryRewardItems,
|
|
StatusCounts: statusCounts,
|
|
PaymentEvents: gateway.countReceipt(receiptExpense),
|
|
IncomeEvents: gateway.countReceipt(receiptIncome),
|
|
LimitedRewards: limitedRewards,
|
|
ExhaustedLimitedRewards: exhaustedLimitedRewards,
|
|
TotalLimitViolations: totalLimitViolations,
|
|
UserLimitViolations: userLimitViolations,
|
|
IssuedCountMismatches: issuedCountMismatches,
|
|
TopRewards: topRewards,
|
|
ElapsedMillis: time.Since(startedAt).Milliseconds(),
|
|
}
|
|
out, err := json.MarshalIndent(summary, "", " ")
|
|
must(err, "marshal summary")
|
|
fmt.Println(string(out))
|
|
}
|
|
|
|
func specs() []categorySpec {
|
|
return []categorySpec{
|
|
{Category: "CLASSIC", RewardCount: 8, PriceOneGold: 3, PriceTenGold: 30, PriceFiftyGold: 150, GoldBase: 10},
|
|
{Category: "LUXURY", RewardCount: 8, PriceOneGold: 30, PriceTenGold: 300, PriceFiftyGold: 1500, GoldBase: 100},
|
|
{Category: "ADVANCED", RewardCount: 12, PriceOneGold: 100, PriceTenGold: 1000, PriceFiftyGold: 5000, GoldBase: 500},
|
|
}
|
|
}
|
|
|
|
func seedRandomConfig(ctx context.Context, service *wheel.Service, rng *rand.Rand) error {
|
|
categories := make([]wheel.CategoryConfigInput, 0, len(specs()))
|
|
for _, spec := range specs() {
|
|
probabilities := randomProbabilities(spec.RewardCount, rng)
|
|
rewards := make([]wheel.RewardConfigInput, 0, spec.RewardCount)
|
|
for index := 0; index < spec.RewardCount; index++ {
|
|
totalLimit := int64(0)
|
|
userLimit := int64(0)
|
|
if index == 0 {
|
|
totalLimit = int64(120 + rng.Intn(81))
|
|
userLimit = 1
|
|
}
|
|
if index == 1 {
|
|
totalLimit = int64(260 + rng.Intn(121))
|
|
userLimit = 2
|
|
}
|
|
rewards = append(rewards, wheel.RewardConfigInput{
|
|
Enabled: true,
|
|
Sort: index + 1,
|
|
RewardType: "GOLD",
|
|
GoldAmount: spec.GoldBase * int64(spec.RewardCount-index),
|
|
Probability: probabilities[index],
|
|
TotalLimit: totalLimit,
|
|
UserLimit: userLimit,
|
|
})
|
|
}
|
|
categories = append(categories, wheel.CategoryConfigInput{
|
|
Category: spec.Category,
|
|
Enabled: true,
|
|
PriceOneGold: spec.PriceOneGold,
|
|
PriceTenGold: spec.PriceTenGold,
|
|
PriceFiftyGold: spec.PriceFiftyGold,
|
|
Rewards: rewards,
|
|
})
|
|
}
|
|
_, err := service.SaveConfig(ctx, wheel.SaveConfigRequest{
|
|
SysOrigin: simSysOrigin,
|
|
Enabled: true,
|
|
Timezone: "Asia/Riyadh",
|
|
Categories: categories,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func randomProbabilities(count int, rng *rand.Rand) []int {
|
|
raw := make([]int, count)
|
|
for i := 0; i < count; i++ {
|
|
raw[i] = rng.Intn(1000) + 1
|
|
}
|
|
raw[0] += 7000 + rng.Intn(3000)
|
|
raw[1] += 3500 + rng.Intn(2000)
|
|
|
|
rawTotal := 0
|
|
for _, value := range raw {
|
|
rawTotal += value
|
|
}
|
|
result := make([]int, count)
|
|
assigned := 0
|
|
for i, value := range raw {
|
|
probability := value * probabilityTotal / rawTotal
|
|
if probability <= 0 {
|
|
probability = 1
|
|
}
|
|
result[i] = probability
|
|
assigned += probability
|
|
}
|
|
result[0] += probabilityTotal - assigned
|
|
return result
|
|
}
|
|
|
|
func randomDrawTimes(rng *rand.Rand) int {
|
|
switch rng.Intn(3) {
|
|
case 0:
|
|
return 1
|
|
case 1:
|
|
return 10
|
|
default:
|
|
return 50
|
|
}
|
|
}
|
|
|
|
func loadStatusCounts(ctx context.Context, db *gorm.DB) map[string]int64 {
|
|
var rows []struct {
|
|
Status string
|
|
Count int64
|
|
}
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT status, COUNT(*) AS count
|
|
FROM wheel_draw_record
|
|
WHERE sys_origin = ?
|
|
GROUP BY status
|
|
`, simSysOrigin).Scan(&rows).Error, "load status counts")
|
|
result := map[string]int64{}
|
|
for _, row := range rows {
|
|
result[row.Status] = row.Count
|
|
}
|
|
return result
|
|
}
|
|
|
|
func loadTopRewards(ctx context.Context, db *gorm.DB, limit int) []topRewardSummary {
|
|
var rows []topRewardSummary
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT
|
|
c.category AS category,
|
|
c.sort AS sort,
|
|
c.id AS reward_id,
|
|
c.gold_amount AS gold_amount,
|
|
c.probability AS probability,
|
|
c.total_limit AS total_limit,
|
|
c.user_limit AS user_limit,
|
|
c.issued_count AS issued_count,
|
|
COUNT(r.id) AS actual_count
|
|
FROM wheel_reward_config c
|
|
LEFT JOIN wheel_draw_record r
|
|
ON r.reward_config_id = c.id
|
|
AND r.status IN ('PENDING', 'GRANTING', 'SUCCESS', 'FAILED')
|
|
WHERE c.sys_origin = ?
|
|
GROUP BY c.category, c.sort, c.id, c.gold_amount, c.probability, c.total_limit, c.user_limit, c.issued_count
|
|
ORDER BY actual_count DESC, c.category ASC, c.sort ASC
|
|
LIMIT ?
|
|
`, simSysOrigin, limit).Scan(&rows).Error, "load top rewards")
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
if rows[i].ActualCount == rows[j].ActualCount {
|
|
return rows[i].RewardID < rows[j].RewardID
|
|
}
|
|
return rows[i].ActualCount > rows[j].ActualCount
|
|
})
|
|
return rows
|
|
}
|
|
|
|
func countTotalLimitViolations(ctx context.Context, db *gorm.DB) int {
|
|
var count int64
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT COUNT(*) FROM (
|
|
SELECT c.id
|
|
FROM wheel_reward_config c
|
|
LEFT JOIN wheel_draw_record r
|
|
ON r.reward_config_id = c.id
|
|
AND r.status IN ('PENDING', 'GRANTING', 'SUCCESS', 'FAILED')
|
|
WHERE c.sys_origin = ?
|
|
AND c.total_limit > 0
|
|
GROUP BY c.id, c.total_limit
|
|
HAVING COUNT(r.id) > c.total_limit
|
|
) t
|
|
`, simSysOrigin).Scan(&count).Error, "count total limit violations")
|
|
return int(count)
|
|
}
|
|
|
|
func countUserLimitViolations(ctx context.Context, db *gorm.DB) int {
|
|
var count int64
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT COUNT(*) FROM (
|
|
SELECT r.reward_config_id, r.user_id
|
|
FROM wheel_draw_record r
|
|
JOIN wheel_reward_config c ON c.id = r.reward_config_id
|
|
WHERE r.sys_origin = ?
|
|
AND c.user_limit > 0
|
|
AND r.status IN ('PENDING', 'GRANTING', 'SUCCESS', 'FAILED')
|
|
GROUP BY r.reward_config_id, r.user_id, c.user_limit
|
|
HAVING COUNT(*) > c.user_limit
|
|
) t
|
|
`, simSysOrigin).Scan(&count).Error, "count user limit violations")
|
|
return int(count)
|
|
}
|
|
|
|
func countIssuedCountMismatches(ctx context.Context, db *gorm.DB) int {
|
|
var count int64
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT COUNT(*) FROM (
|
|
SELECT c.id
|
|
FROM wheel_reward_config c
|
|
LEFT JOIN wheel_draw_record r
|
|
ON r.reward_config_id = c.id
|
|
AND r.status IN ('PENDING', 'GRANTING', 'SUCCESS', 'FAILED')
|
|
WHERE c.sys_origin = ?
|
|
GROUP BY c.id, c.issued_count
|
|
HAVING c.issued_count <> COUNT(r.id)
|
|
) t
|
|
`, simSysOrigin).Scan(&count).Error, "count issued count mismatches")
|
|
return int(count)
|
|
}
|
|
|
|
func countLimitedRewards(ctx context.Context, db *gorm.DB) (int, int) {
|
|
var rows []struct {
|
|
TotalLimit int64
|
|
IssuedCount int64
|
|
}
|
|
must(db.WithContext(ctx).Raw(`
|
|
SELECT total_limit AS total_limit, issued_count AS issued_count
|
|
FROM wheel_reward_config
|
|
WHERE sys_origin = ?
|
|
AND total_limit > 0
|
|
`, simSysOrigin).Scan(&rows).Error, "count limited rewards")
|
|
exhausted := 0
|
|
for _, row := range rows {
|
|
if row.IssuedCount >= row.TotalLimit {
|
|
exhausted++
|
|
}
|
|
}
|
|
return len(rows), exhausted
|
|
}
|
|
|
|
func connectMySQL(ctx context.Context) (*gorm.DB, string, error) {
|
|
candidates := uniqueStrings([]string{
|
|
strings.TrimSpace(os.Getenv("CHATAPP_STORE_MYSQL_DSN")),
|
|
"root:123456@tcp(127.0.0.1:13306)/likei?charset=utf8mb4&parseTime=True&loc=Asia%2FRiyadh",
|
|
"root:root@tcp(127.0.0.1:3306)/chatapp3?charset=utf8mb4&parseTime=True&loc=Local",
|
|
})
|
|
var errs []string
|
|
for _, dsn := range candidates {
|
|
if dsn == "" {
|
|
continue
|
|
}
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("%s: %v", dsnLabel(dsn), err))
|
|
continue
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("%s: %v", dsnLabel(dsn), err))
|
|
continue
|
|
}
|
|
if err := sqlDB.PingContext(ctx); err != nil {
|
|
_ = sqlDB.Close()
|
|
errs = append(errs, fmt.Sprintf("%s: %v", dsnLabel(dsn), err))
|
|
continue
|
|
}
|
|
return db, dsn, nil
|
|
}
|
|
return nil, "", fmt.Errorf("no local mysql dsn available: %s", strings.Join(errs, "; "))
|
|
}
|
|
|
|
func ensureWheelSchema(db *gorm.DB) error {
|
|
return db.AutoMigrate(
|
|
&model.WheelConfig{},
|
|
&model.WheelPoolConfig{},
|
|
&model.WheelRewardConfig{},
|
|
&model.WheelDrawRecord{},
|
|
&model.ActivityIdempotency{},
|
|
)
|
|
}
|
|
|
|
func cleanup(ctx context.Context, db *gorm.DB, sysOrigin string) error {
|
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var configIDs []int64
|
|
if err := tx.Model(&model.WheelConfig{}).
|
|
Where("sys_origin = ?", sysOrigin).
|
|
Pluck("id", &configIDs).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("sys_origin = ?", sysOrigin).Delete(&model.WheelDrawRecord{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("sys_origin = ?", sysOrigin).Delete(&model.ActivityIdempotency{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(configIDs) == 0 {
|
|
return nil
|
|
}
|
|
if err := tx.Where("config_id IN ?", configIDs).Delete(&model.WheelRewardConfig{}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Where("config_id IN ?", configIDs).Delete(&model.WheelPoolConfig{}).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Where("id IN ?", configIDs).Delete(&model.WheelConfig{}).Error
|
|
})
|
|
}
|
|
|
|
func appErrorCode(err error) string {
|
|
var appErr *common.AppError
|
|
if errors.As(err, &appErr) {
|
|
return appErr.Code
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func dsnLabel(dsn string) string {
|
|
if at := strings.Index(dsn, "@tcp("); at >= 0 {
|
|
rest := dsn[at+len("@tcp("):]
|
|
hostEnd := strings.Index(rest, ")")
|
|
if hostEnd >= 0 {
|
|
host := rest[:hostEnd]
|
|
dbName := ""
|
|
after := rest[hostEnd+1:]
|
|
if strings.HasPrefix(after, "/") {
|
|
dbPart := strings.TrimPrefix(after, "/")
|
|
if question := strings.Index(dbPart, "?"); question >= 0 {
|
|
dbPart = dbPart[:question]
|
|
}
|
|
dbName = dbPart
|
|
}
|
|
if dbName != "" {
|
|
return host + "/" + dbName
|
|
}
|
|
return host
|
|
}
|
|
}
|
|
return "custom-dsn"
|
|
}
|
|
|
|
func uniqueStrings(values []string) []string {
|
|
seen := map[string]struct{}{}
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func envInt(name string, fallback int) int {
|
|
raw := strings.TrimSpace(os.Getenv(name))
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(raw)
|
|
if err != nil || parsed <= 0 {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func envInt64(name string, fallback int64) int64 {
|
|
raw := strings.TrimSpace(os.Getenv(name))
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func must(err error, step string) {
|
|
if err != nil {
|
|
log.Fatalf("%s: %v", step, err)
|
|
}
|
|
}
|
|
|
|
func assert(ok bool, message string) {
|
|
if !ok {
|
|
log.Fatal(message)
|
|
}
|
|
}
|