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
a74491fbe3
commit
6ca908eaab
2 changed files with 65 additions and 16 deletions
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
@ -19,13 +21,18 @@ func NewStore(dbPath string) (*Store, error) {
|
||||||
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// foreign_keys(1): enable FK constraints (disabled by default in SQLite)
|
// foreign_keys(1): enable FK constraints
|
||||||
// _busy_timeout=5000: wait up to 5s when DB is locked (default=0, fails immediately)
|
// busy_timeout(5000): wait up to 5s for locks instead of failing immediately
|
||||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)&_busy_timeout=5000")
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
db.SetMaxIdleConns(1)
|
||||||
|
|
||||||
if err := db.Ping(); err != nil {
|
if err := db.Ping(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
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 {
|
func (s *Store) RegisterApp(appName string) error {
|
||||||
query := `INSERT INTO apps (appName) VALUES (?) ON CONFLICT(appName) DO NOTHING`
|
query := `INSERT INTO apps (appName) VALUES (?) ON CONFLICT(appName) DO NOTHING`
|
||||||
_, err := s.db.Exec(query, appName)
|
_, err := s.execWrite(query, appName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +121,7 @@ func (s *Store) AddSubscription(sub Subscription) error {
|
||||||
vapidPrivateKey = &sub.WebPush.VapidPrivateKey
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,16 +204,29 @@ func (s *Store) GetAllApps() ([]App, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var apps []App
|
appNames := make([]string, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var app App
|
var appName string
|
||||||
if err := rows.Scan(&app.AppName); err != nil {
|
if err := rows.Scan(&appName); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +235,7 @@ func (s *Store) GetAllApps() ([]App, error) {
|
||||||
apps = append(apps, app)
|
apps = append(apps, app)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps, rows.Err()
|
return apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetSignalGroup(appName string) (*SignalSubscription, error) {
|
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 (?, ?, ?)
|
query := `INSERT INTO signal_groups (appName, groupId, account) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(appName) DO UPDATE SET groupId=excluded.groupId, account=excluded.account`
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteSubscription(subscriptionID string) error {
|
func (s *Store) DeleteSubscription(subscriptionID string) error {
|
||||||
query := `DELETE FROM subscriptions WHERE id = ?`
|
query := `DELETE FROM subscriptions WHERE id = ?`
|
||||||
_, err := s.db.Exec(query, subscriptionID)
|
_, err := s.execWrite(query, subscriptionID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,6 +322,35 @@ func (s *Store) GetSubscription(subscriptionID string) (*Subscription, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) RemoveApp(appName string) 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
|
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-target="#apps-list"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
{{.Label}}
|
{{.Label}}
|
||||||
<span class="tooltip">Subscription ID: {{(index .Subscriptions 0).ID}}</span>
|
<span class="tooltip">Channel ID: {{(index .Subscriptions 0).ID}}</span>
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="button" class="channel-badge channel-{{.Channel}} badge-inactive"
|
<button type="button" class="channel-badge channel-{{.Channel}} badge-inactive"
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
{{range .Subscriptions}}
|
{{range .Subscriptions}}
|
||||||
<span class="channel-badge channel-{{$channel.Channel}} badge-subscribed">
|
<span class="channel-badge channel-{{$channel.Channel}} badge-subscribed">
|
||||||
{{$channel.Label}}
|
{{$channel.Label}}
|
||||||
<span class="tooltip">Subscription ID: {{.ID}}</span>
|
<span class="tooltip">Channel ID: {{.ID}}</span>
|
||||||
<button type="button" class="btn-delete-sub"
|
<button type="button" class="btn-delete-sub"
|
||||||
hx-delete="/apps/{{$app.AppName}}/subscriptions/{{.ID}}"
|
hx-delete="/apps/{{$app.AppName}}/subscriptions/{{.ID}}"
|
||||||
hx-target="#apps-list"
|
hx-target="#apps-list"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue