github.com/TeaOSLab/EdgeNode@v1.3.8/internal/utils/dbs/db.go (about)

     1  // Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
     2  
     3  package dbs
     4  
     5  import (
     6  	"context"
     7  	"database/sql"
     8  	"errors"
     9  	"fmt"
    10  	teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
    11  	"github.com/TeaOSLab/EdgeNode/internal/events"
    12  	"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
    13  	"github.com/TeaOSLab/EdgeNode/internal/utils/fs"
    14  	_ "github.com/mattn/go-sqlite3"
    15  	"os"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  )
    20  
    21  const (
    22  	SyncMode = "OFF"
    23  )
    24  
    25  var errDBIsClosed = errors.New("the database is closed")
    26  
    27  type DB struct {
    28  	locker *fsutils.Locker
    29  	rawDB  *sql.DB
    30  	dsn    string
    31  
    32  	statusLocker  sync.Mutex
    33  	countUpdating int32
    34  
    35  	isClosing bool
    36  
    37  	enableStat bool
    38  
    39  	batches []*Batch
    40  }
    41  
    42  func OpenWriter(dsn string) (*DB, error) {
    43  	return open(dsn, true)
    44  }
    45  
    46  func OpenReader(dsn string) (*DB, error) {
    47  	return open(dsn, false)
    48  }
    49  
    50  func open(dsn string, lock bool) (*DB, error) {
    51  	if teaconst.IsQuiting {
    52  		return nil, errors.New("can not open database when process is quiting")
    53  	}
    54  
    55  	// decode path
    56  	var path = dsn
    57  	var queryIndex = strings.Index(dsn, "?")
    58  	if queryIndex >= 0 {
    59  		path = path[:queryIndex]
    60  	}
    61  	path = strings.TrimSpace(strings.TrimPrefix(path, "file:"))
    62  
    63  	// locker
    64  	var locker *fsutils.Locker
    65  	if lock {
    66  		locker = fsutils.NewLocker(path)
    67  		err := locker.Lock()
    68  		if err != nil {
    69  			remotelogs.Warn("DB", "lock '"+path+"' failed: "+err.Error())
    70  			locker = nil
    71  		}
    72  	}
    73  
    74  	// check if closed successfully last time, if not we recover it
    75  	var walPath = path + "-wal"
    76  	_, statWalErr := os.Stat(walPath)
    77  	var shouldRecover = statWalErr == nil
    78  
    79  	// open
    80  	rawDB, err := sql.Open("sqlite3", dsn)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	if shouldRecover {
    86  		err = rawDB.Close()
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  
    91  		// open again
    92  		rawDB, err = sql.Open("sqlite3", dsn)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  	}
    97  
    98  	var db = NewDB(rawDB, dsn)
    99  	db.locker = locker
   100  	return db, nil
   101  }
   102  
   103  func NewDB(rawDB *sql.DB, dsn string) *DB {
   104  	var db = &DB{
   105  		rawDB: rawDB,
   106  		dsn:   dsn,
   107  	}
   108  
   109  	events.OnKey(events.EventQuit, fmt.Sprintf("db_%p", db), func() {
   110  		_ = db.Close()
   111  	})
   112  	events.OnKey(events.EventTerminated, fmt.Sprintf("db_%p", db), func() {
   113  		_ = db.Close()
   114  	})
   115  
   116  	return db
   117  }
   118  
   119  func (this *DB) SetMaxOpenConns(n int) {
   120  	this.rawDB.SetMaxOpenConns(n)
   121  }
   122  
   123  func (this *DB) EnableStat(b bool) {
   124  	this.enableStat = b
   125  }
   126  
   127  func (this *DB) Begin() (*sql.Tx, error) {
   128  	// check database status
   129  	if this.BeginUpdating() {
   130  		defer this.EndUpdating()
   131  	} else {
   132  		return nil, errDBIsClosed
   133  	}
   134  
   135  	return this.rawDB.Begin()
   136  }
   137  
   138  func (this *DB) Prepare(query string) (*Stmt, error) {
   139  	stmt, err := this.rawDB.Prepare(query)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	var s = NewStmt(this, stmt, query)
   145  	if this.enableStat {
   146  		s.EnableStat()
   147  	}
   148  	return s, nil
   149  }
   150  
   151  func (this *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
   152  	// check database status
   153  	if this.BeginUpdating() {
   154  		defer this.EndUpdating()
   155  	} else {
   156  		return nil, errDBIsClosed
   157  	}
   158  
   159  	if this.enableStat {
   160  		defer SharedQueryStatManager.AddQuery(query).End()
   161  	}
   162  
   163  	return this.rawDB.ExecContext(ctx, query, args...)
   164  }
   165  
   166  func (this *DB) Exec(query string, args ...any) (sql.Result, error) {
   167  	// check database status
   168  	if this.BeginUpdating() {
   169  		defer this.EndUpdating()
   170  	} else {
   171  		return nil, errDBIsClosed
   172  	}
   173  
   174  	if this.enableStat {
   175  		defer SharedQueryStatManager.AddQuery(query).End()
   176  	}
   177  	return this.rawDB.Exec(query, args...)
   178  }
   179  
   180  func (this *DB) Query(query string, args ...any) (*sql.Rows, error) {
   181  	if this.enableStat {
   182  		defer SharedQueryStatManager.AddQuery(query).End()
   183  	}
   184  	return this.rawDB.Query(query, args...)
   185  }
   186  
   187  func (this *DB) QueryRow(query string, args ...any) *sql.Row {
   188  	if this.enableStat {
   189  		defer SharedQueryStatManager.AddQuery(query).End()
   190  	}
   191  	return this.rawDB.QueryRow(query, args...)
   192  }
   193  
   194  // Close the database
   195  func (this *DB) Close() error {
   196  	// check database status
   197  	this.statusLocker.Lock()
   198  	if this.isClosing {
   199  		this.statusLocker.Unlock()
   200  		return nil
   201  	}
   202  	this.isClosing = true
   203  	this.statusLocker.Unlock()
   204  
   205  	// waiting for updating operations to finish
   206  	var maxLoops = 5_000
   207  	for {
   208  		this.statusLocker.Lock()
   209  		var countUpdating = this.countUpdating
   210  		this.statusLocker.Unlock()
   211  		if countUpdating <= 0 {
   212  			break
   213  		}
   214  		time.Sleep(1 * time.Millisecond)
   215  
   216  		maxLoops--
   217  		if maxLoops <= 0 {
   218  			break
   219  		}
   220  	}
   221  
   222  	for _, batch := range this.batches {
   223  		batch.close()
   224  	}
   225  
   226  	events.Remove(fmt.Sprintf("db_%p", this))
   227  
   228  	defer func() {
   229  		if this.locker != nil {
   230  			_ = this.locker.Release()
   231  		}
   232  	}()
   233  
   234  	// print log
   235  	/**if len(this.dsn) > 0 {
   236  		u, _ := url.Parse(this.dsn)
   237  		if u != nil && len(u.Path) > 0 {
   238  			remotelogs.Debug("DB", "close '"+u.Path)
   239  		}
   240  	}**/
   241  
   242  	return this.rawDB.Close()
   243  }
   244  
   245  func (this *DB) BeginUpdating() bool {
   246  	this.statusLocker.Lock()
   247  	defer this.statusLocker.Unlock()
   248  
   249  	if this.isClosing {
   250  		return false
   251  	}
   252  
   253  	this.countUpdating++
   254  	return true
   255  }
   256  
   257  func (this *DB) EndUpdating() {
   258  	this.statusLocker.Lock()
   259  	this.countUpdating--
   260  	this.statusLocker.Unlock()
   261  }
   262  
   263  func (this *DB) RawDB() *sql.DB {
   264  	return this.rawDB
   265  }