mirror of
https://github.com/lone-cloud/prism
synced 2026-06-03 08:43:10 -07:00
correcting sqlite limitation on bursty requests which lock the DB without a busy timeout
This commit is contained in:
parent
061fcb305f
commit
a96a814661
2 changed files with 65 additions and 16 deletions
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
|
@ -19,13 +21,18 @@ func NewStore(dbPath string) (*Store, error) {
|
|||
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
// foreign_keys(1): enable FK constraints (disabled by default in SQLite)
|
||||
// _busy_timeout=5000: wait up to 5s when DB is locked (default=0, fails immediately)
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)&_busy_timeout=5000")
|
||||
// foreign_keys(1): enable FK constraints
|
||||
// busy_timeout(5000): wait up to 5s for locks instead of failing immediately
|
||||
// journal_mode(WAL): improves concurrent read/write behavior
|
||||
// synchronous(FULL): durability-first mode; safest on sudden power loss
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=synchronous(FULL)")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
|
@ -84,7 +91,7 @@ func (s *Store) GetDB() *sql.DB {
|
|||
|
||||
func (s *Store) RegisterApp(appName string) error {
|
||||
query := `INSERT INTO apps (appName) VALUES (?) ON CONFLICT(appName) DO NOTHING`
|
||||
_, err := s.db.Exec(query, appName)
|
||||
_, err := s.execWrite(query, appName)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +121,7 @@ func (s *Store) AddSubscription(sub Subscription) error {
|
|||
vapidPrivateKey = &sub.WebPush.VapidPrivateKey
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(query, sub.ID, sub.AppName, sub.Channel, signalGroupID, signalAccount, telegramChatID, pushEndpoint, p256dh, auth, vapidPrivateKey)
|
||||
_, err := s.execWrite(query, sub.ID, sub.AppName, sub.Channel, signalGroupID, signalAccount, telegramChatID, pushEndpoint, p256dh, auth, vapidPrivateKey)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -197,16 +204,29 @@ func (s *Store) GetAllApps() ([]App, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []App
|
||||
appNames := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var app App
|
||||
if err := rows.Scan(&app.AppName); err != nil {
|
||||
var appName string
|
||||
if err := rows.Scan(&appName); err != nil {
|
||||
_ = rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
appNames = append(appNames, appName)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
_ = rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subs, err := s.GetSubscriptions(app.AppName)
|
||||
apps := make([]App, 0, len(appNames))
|
||||
for _, appName := range appNames {
|
||||
app := App{AppName: appName}
|
||||
|
||||
subs, err := s.GetSubscriptions(appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -215,7 +235,7 @@ func (s *Store) GetAllApps() ([]App, error) {
|
|||
apps = append(apps, app)
|
||||
}
|
||||
|
||||
return apps, rows.Err()
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSignalGroup(appName string) (*SignalSubscription, error) {
|
||||
|
|
@ -243,13 +263,13 @@ func (s *Store) SaveSignalGroup(appName string, sub *SignalSubscription) error {
|
|||
|
||||
query := `INSERT INTO signal_groups (appName, groupId, account) VALUES (?, ?, ?)
|
||||
ON CONFLICT(appName) DO UPDATE SET groupId=excluded.groupId, account=excluded.account`
|
||||
_, err := s.db.Exec(query, appName, sub.GroupID, sub.Account)
|
||||
_, err := s.execWrite(query, appName, sub.GroupID, sub.Account)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSubscription(subscriptionID string) error {
|
||||
query := `DELETE FROM subscriptions WHERE id = ?`
|
||||
_, err := s.db.Exec(query, subscriptionID)
|
||||
_, err := s.execWrite(query, subscriptionID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +322,35 @@ func (s *Store) GetSubscription(subscriptionID string) (*Subscription, error) {
|
|||
}
|
||||
|
||||
func (s *Store) RemoveApp(appName string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM apps WHERE appName = ?`, appName)
|
||||
_, err := s.execWrite(`DELETE FROM apps WHERE appName = ?`, appName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) execWrite(query string, args ...any) (sql.Result, error) {
|
||||
const maxAttempts = 4
|
||||
delay := 50 * time.Millisecond
|
||||
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
result, err := s.db.Exec(query, args...)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if !isSQLiteBusyError(err) || attempt == maxAttempts-1 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("write failed")
|
||||
}
|
||||
|
||||
func isSQLiteBusyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
message := strings.ToLower(err.Error())
|
||||
return strings.Contains(message, "sqlite_busy") || strings.Contains(message, "database is locked")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
hx-target="#apps-list"
|
||||
hx-swap="innerHTML">
|
||||
{{.Label}}
|
||||
<span class="tooltip">Subscription ID: {{(index .Subscriptions 0).ID}}</span>
|
||||
<span class="tooltip">Channel ID: {{(index .Subscriptions 0).ID}}</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="button" class="channel-badge channel-{{.Channel}} badge-inactive"
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
{{range .Subscriptions}}
|
||||
<span class="channel-badge channel-{{$channel.Channel}} badge-subscribed">
|
||||
{{$channel.Label}}
|
||||
<span class="tooltip">Subscription ID: {{.ID}}</span>
|
||||
<span class="tooltip">Channel ID: {{.ID}}</span>
|
||||
<button type="button" class="btn-delete-sub"
|
||||
hx-delete="/apps/{{$app.AppName}}/subscriptions/{{.ID}}"
|
||||
hx-target="#apps-list"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue