mirror of
https://github.com/gofiber/storage.git
synced 2025-10-24 08:53:08 +08:00
Revert DisableStartupCheck changes for select drivers
This commit is contained in:
@@ -73,6 +73,11 @@ type Config struct {
|
|||||||
//
|
//
|
||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,5 +85,6 @@ type Config struct {
|
|||||||
```go
|
```go
|
||||||
var ConfigDefault = Config{
|
var ConfigDefault = Config{
|
||||||
Servers: "127.0.0.1:11211",
|
Servers: "127.0.0.1:11211",
|
||||||
|
DisableStartupCheck: false,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// The socket read/write timeout.
|
// The socket read/write timeout.
|
||||||
//
|
//
|
||||||
// Optional. Default is 100 * time.Millisecond
|
// Optional. Default is 100 * time.Millisecond
|
||||||
@@ -31,9 +36,10 @@ type Config struct {
|
|||||||
|
|
||||||
// ConfigDefault is the default config
|
// ConfigDefault is the default config
|
||||||
var ConfigDefault = Config{
|
var ConfigDefault = Config{
|
||||||
Servers: "127.0.0.1:11211",
|
Servers: "127.0.0.1:11211",
|
||||||
timeout: 100 * time.Millisecond,
|
timeout: 100 * time.Millisecond,
|
||||||
maxIdleConns: 2,
|
maxIdleConns: 2,
|
||||||
|
DisableStartupCheck: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to set default values
|
// Helper function to set default values
|
||||||
|
|||||||
@@ -30,15 +30,17 @@ func New(config ...Config) *Storage {
|
|||||||
db.Timeout = cfg.timeout
|
db.Timeout = cfg.timeout
|
||||||
db.MaxIdleConns = cfg.maxIdleConns
|
db.MaxIdleConns = cfg.maxIdleConns
|
||||||
|
|
||||||
// Ping database to ensure a connection has been made
|
if !cfg.DisableStartupCheck {
|
||||||
if err := db.Ping(); err != nil {
|
// Ping database to ensure a connection has been made
|
||||||
panic(err)
|
if err := db.Ping(); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Reset {
|
|
||||||
if err := db.DeleteAll(); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Reset {
|
||||||
|
if err := db.DeleteAll(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create storage
|
// Create storage
|
||||||
|
|||||||
@@ -178,6 +178,16 @@ func Test_Memcache_Reset(t *testing.T) {
|
|||||||
require.Zero(t, len(result))
|
require.Zero(t, len(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Memcache_New_DisableStartupCheck(t *testing.T) {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
store := New(Config{
|
||||||
|
Servers: "127.0.0.1:11210",
|
||||||
|
DisableStartupCheck: true,
|
||||||
|
})
|
||||||
|
require.NotNil(t, store)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_Memcache_Close(t *testing.T) {
|
func Test_Memcache_Close(t *testing.T) {
|
||||||
testStore := newTestStore(t)
|
testStore := newTestStore(t)
|
||||||
require.Nil(t, testStore.Close())
|
require.Nil(t, testStore.Close())
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// Time before deleting expired keys
|
// Time before deleting expired keys
|
||||||
//
|
//
|
||||||
// Optional. Default is 10 * time.Second
|
// Optional. Default is 10 * time.Second
|
||||||
@@ -143,6 +148,7 @@ var ConfigDefault = Config{
|
|||||||
Database: "fiber",
|
Database: "fiber",
|
||||||
Table: "fiber_storage",
|
Table: "fiber_storage",
|
||||||
Reset: false,
|
Reset: false,
|
||||||
|
DisableStartupCheck: false,
|
||||||
GCInterval: 10 * time.Second,
|
GCInterval: 10 * time.Second,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// Time before deleting expired keys
|
// Time before deleting expired keys
|
||||||
//
|
//
|
||||||
// Optional. Default is 10 * time.Second
|
// Optional. Default is 10 * time.Second
|
||||||
@@ -69,17 +74,18 @@ type Config struct {
|
|||||||
|
|
||||||
// ConfigDefault is the default config
|
// ConfigDefault is the default config
|
||||||
var ConfigDefault = Config{
|
var ConfigDefault = Config{
|
||||||
Db: nil,
|
Db: nil,
|
||||||
ConnectionURI: "",
|
ConnectionURI: "",
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 3306,
|
Port: 3306,
|
||||||
Database: "fiber",
|
Database: "fiber",
|
||||||
Table: "fiber_storage",
|
Table: "fiber_storage",
|
||||||
Reset: false,
|
Reset: false,
|
||||||
GCInterval: 10 * time.Second,
|
DisableStartupCheck: false,
|
||||||
maxOpenConns: 100,
|
GCInterval: 10 * time.Second,
|
||||||
maxIdleConns: 100,
|
maxOpenConns: 100,
|
||||||
connMaxLifetime: 1 * time.Second,
|
maxIdleConns: 100,
|
||||||
|
connMaxLifetime: 1 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) dsn() string {
|
func (c Config) dsn() string {
|
||||||
|
|||||||
@@ -63,26 +63,28 @@ func New(config ...Config) *Storage {
|
|||||||
db.SetConnMaxLifetime(cfg.connMaxLifetime)
|
db.SetConnMaxLifetime(cfg.connMaxLifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping database to ensure a connection has been made
|
if !cfg.DisableStartupCheck {
|
||||||
if err := db.Ping(); err != nil {
|
// Ping database to ensure a connection has been made
|
||||||
panic(err)
|
if err := db.Ping(); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
// Drop table if Clear set to true
|
|
||||||
if cfg.Reset {
|
|
||||||
query := fmt.Sprintf(dropQuery, cfg.Table)
|
|
||||||
if _, err = db.Exec(query); err != nil {
|
|
||||||
_ = db.Close()
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Init database queries
|
// Drop table if Clear set to true
|
||||||
for _, query := range initQuery {
|
if cfg.Reset {
|
||||||
query = fmt.Sprintf(query, cfg.Table)
|
query := fmt.Sprintf(dropQuery, cfg.Table)
|
||||||
if _, err := db.Exec(query); err != nil {
|
if _, err = db.Exec(query); err != nil {
|
||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
panic(err)
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init database queries
|
||||||
|
for _, query := range initQuery {
|
||||||
|
query = fmt.Sprintf(query, cfg.Table)
|
||||||
|
if _, err := db.Exec(query); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +100,9 @@ func New(config ...Config) *Storage {
|
|||||||
sqlGC: fmt.Sprintf("DELETE FROM %s WHERE e <= ? AND e != 0", cfg.Table),
|
sqlGC: fmt.Sprintf("DELETE FROM %s WHERE e <= ? AND e != 0", cfg.Table),
|
||||||
}
|
}
|
||||||
|
|
||||||
store.checkSchema(cfg.Table)
|
if !cfg.DisableStartupCheck {
|
||||||
|
store.checkSchema(cfg.Table)
|
||||||
|
}
|
||||||
|
|
||||||
// Start garbage collector
|
// Start garbage collector
|
||||||
go store.gcTicker()
|
go store.gcTicker()
|
||||||
|
|||||||
@@ -108,6 +108,18 @@ func Test_MYSQL_New(t *testing.T) {
|
|||||||
defer newConfigStore.Close()
|
defer newConfigStore.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_MYSQL_New_DisableStartupCheck(t *testing.T) {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
store := New(Config{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 3308,
|
||||||
|
DisableStartupCheck: true,
|
||||||
|
})
|
||||||
|
require.NotNil(t, store)
|
||||||
|
defer store.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_MYSQL_GC(t *testing.T) {
|
func Test_MYSQL_GC(t *testing.T) {
|
||||||
testVal := []byte("doe")
|
testVal := []byte("doe")
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// Time before deleting expired keys
|
// Time before deleting expired keys
|
||||||
//
|
//
|
||||||
// Optional. Default is 10 * time.Second
|
// Optional. Default is 10 * time.Second
|
||||||
@@ -135,6 +140,7 @@ var ConfigDefault = Config{
|
|||||||
Table: "fiber_storage",
|
Table: "fiber_storage",
|
||||||
SSLMode: "disable",
|
SSLMode: "disable",
|
||||||
Reset: false,
|
Reset: false,
|
||||||
|
DisableStartupCheck: false,
|
||||||
GCInterval: 10 * time.Second,
|
GCInterval: 10 * time.Second,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// Time before deleting expired keys
|
// Time before deleting expired keys
|
||||||
//
|
//
|
||||||
// Optional. Default is 10 * time.Second
|
// Optional. Default is 10 * time.Second
|
||||||
@@ -69,14 +74,15 @@ type Config struct {
|
|||||||
|
|
||||||
// ConfigDefault is the default config
|
// ConfigDefault is the default config
|
||||||
var ConfigDefault = Config{
|
var ConfigDefault = Config{
|
||||||
ConnectionURI: "",
|
ConnectionURI: "",
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 5432,
|
Port: 5432,
|
||||||
Database: "fiber",
|
Database: "fiber",
|
||||||
Table: "fiber_storage",
|
Table: "fiber_storage",
|
||||||
SSLMode: "disable",
|
SSLMode: "disable",
|
||||||
Reset: false,
|
Reset: false,
|
||||||
GCInterval: 10 * time.Second,
|
DisableStartupCheck: false,
|
||||||
|
GCInterval: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) getDSN() string {
|
func (c *Config) getDSN() string {
|
||||||
|
|||||||
@@ -68,43 +68,45 @@ func New(config ...Config) *Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping database
|
if !cfg.DisableStartupCheck {
|
||||||
if err := db.Ping(context.Background()); err != nil {
|
// Ping database
|
||||||
panic(err)
|
if err := db.Ping(context.Background()); err != nil {
|
||||||
}
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse out schema in config, if provided
|
// Parse out schema in config, if provided
|
||||||
schema := "public"
|
schema := "public"
|
||||||
tableName := cfg.Table
|
tableName := cfg.Table
|
||||||
if strings.Contains(cfg.Table, ".") {
|
if strings.Contains(cfg.Table, ".") {
|
||||||
schema = strings.Split(cfg.Table, ".")[0]
|
schema = strings.Split(cfg.Table, ".")[0]
|
||||||
tableName = strings.Split(cfg.Table, ".")[1]
|
tableName = strings.Split(cfg.Table, ".")[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop table if set to true
|
// Drop table if set to true
|
||||||
if cfg.Reset {
|
if cfg.Reset {
|
||||||
if _, err := db.Exec(context.Background(), fmt.Sprintf(dropQuery, cfg.Table)); err != nil {
|
if _, err := db.Exec(context.Background(), fmt.Sprintf(dropQuery, cfg.Table)); err != nil {
|
||||||
|
db.Close()
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if table exists
|
||||||
|
tableExists := false
|
||||||
|
row := db.QueryRow(context.Background(), fmt.Sprintf(checkTableExistsQuery, schema, tableName))
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
db.Close()
|
db.Close()
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
tableExists = count > 0
|
||||||
|
|
||||||
// Determine if table exists
|
// Init database queries
|
||||||
tableExists := false
|
if !tableExists {
|
||||||
row := db.QueryRow(context.Background(), fmt.Sprintf(checkTableExistsQuery, schema, tableName))
|
for _, query := range initQuery {
|
||||||
var count int
|
if _, err := db.Exec(context.Background(), fmt.Sprintf(query, cfg.Table)); err != nil {
|
||||||
if err := row.Scan(&count); err != nil {
|
db.Close()
|
||||||
db.Close()
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
tableExists = count > 0
|
|
||||||
|
|
||||||
// Init database queries
|
|
||||||
if !tableExists {
|
|
||||||
for _, query := range initQuery {
|
|
||||||
if _, err := db.Exec(context.Background(), fmt.Sprintf(query, cfg.Table)); err != nil {
|
|
||||||
db.Close()
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,9 @@ func New(config ...Config) *Storage {
|
|||||||
sqlGC: fmt.Sprintf("DELETE FROM %s WHERE e <= $1 AND e != 0", cfg.Table),
|
sqlGC: fmt.Sprintf("DELETE FROM %s WHERE e <= $1 AND e != 0", cfg.Table),
|
||||||
}
|
}
|
||||||
|
|
||||||
store.checkSchema(cfg.Table)
|
if !cfg.DisableStartupCheck {
|
||||||
|
store.checkSchema(cfg.Table)
|
||||||
|
}
|
||||||
|
|
||||||
// Start garbage collector
|
// Start garbage collector
|
||||||
go store.gcTicker()
|
go store.gcTicker()
|
||||||
|
|||||||
@@ -202,6 +202,18 @@ func TestNoCreateUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Postgres_New_DisableStartupCheck(t *testing.T) {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
store := New(Config{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 65432,
|
||||||
|
DisableStartupCheck: true,
|
||||||
|
})
|
||||||
|
require.NotNil(t, store)
|
||||||
|
defer store.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
func Test_Should_Panic_On_Wrong_Schema(t *testing.T) {
|
func Test_Should_Panic_On_Wrong_Schema(t *testing.T) {
|
||||||
testStore := newTestStore(t)
|
testStore := newTestStore(t)
|
||||||
defer testStore.Close()
|
defer testStore.Close()
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// TLS Config to use. When set TLS will be negotiated.
|
// TLS Config to use. When set TLS will be negotiated.
|
||||||
//
|
//
|
||||||
// Optional. Default is nil
|
// Optional. Default is nil
|
||||||
@@ -208,6 +213,7 @@ var ConfigDefault = Config{
|
|||||||
URL: "",
|
URL: "",
|
||||||
Database: 0,
|
Database: 0,
|
||||||
Reset: false,
|
Reset: false,
|
||||||
|
DisableStartupCheck: false,
|
||||||
TLSConfig: nil,
|
TLSConfig: nil,
|
||||||
PoolSize: 10 * runtime.GOMAXPROCS(0),
|
PoolSize: 10 * runtime.GOMAXPROCS(0),
|
||||||
Addrs: []string{},
|
Addrs: []string{},
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ type Config struct {
|
|||||||
// Optional. Default is false
|
// Optional. Default is false
|
||||||
Reset bool
|
Reset bool
|
||||||
|
|
||||||
|
// DisableStartupCheck skips the initial connection validation during New.
|
||||||
|
//
|
||||||
|
// Optional. Default is false
|
||||||
|
DisableStartupCheck bool
|
||||||
|
|
||||||
// TLS Config to use. When set TLS will be negotiated.
|
// TLS Config to use. When set TLS will be negotiated.
|
||||||
//
|
//
|
||||||
// Optional. Default is nil
|
// Optional. Default is nil
|
||||||
@@ -87,21 +92,22 @@ type Config struct {
|
|||||||
|
|
||||||
// ConfigDefault is the default config
|
// ConfigDefault is the default config
|
||||||
var ConfigDefault = Config{
|
var ConfigDefault = Config{
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 6379,
|
Port: 6379,
|
||||||
Username: "",
|
Username: "",
|
||||||
Password: "",
|
Password: "",
|
||||||
URL: "",
|
URL: "",
|
||||||
Database: 0,
|
Database: 0,
|
||||||
Reset: false,
|
Reset: false,
|
||||||
TLSConfig: nil,
|
DisableStartupCheck: false,
|
||||||
PoolSize: 10 * runtime.GOMAXPROCS(0),
|
TLSConfig: nil,
|
||||||
Addrs: []string{},
|
PoolSize: 10 * runtime.GOMAXPROCS(0),
|
||||||
MasterName: "",
|
Addrs: []string{},
|
||||||
ClientName: "",
|
MasterName: "",
|
||||||
SentinelUsername: "",
|
ClientName: "",
|
||||||
SentinelPassword: "",
|
SentinelUsername: "",
|
||||||
IsClusterMode: false,
|
SentinelPassword: "",
|
||||||
|
IsClusterMode: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to set default values
|
// Helper function to set default values
|
||||||
|
|||||||
@@ -65,16 +65,18 @@ func New(config ...Config) *Storage {
|
|||||||
IsClusterMode: cfg.IsClusterMode,
|
IsClusterMode: cfg.IsClusterMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test connection
|
if !cfg.DisableStartupCheck {
|
||||||
if err := db.Ping(context.Background()).Err(); err != nil {
|
// Test connection
|
||||||
panic(err)
|
if err := db.Ping(context.Background()).Err(); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
// Empty collection if Clear is true
|
|
||||||
if cfg.Reset {
|
|
||||||
if err := db.FlushDB(context.Background()).Err(); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty collection if Clear is true
|
||||||
|
if cfg.Reset {
|
||||||
|
if err := db.FlushDB(context.Background()).Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new store
|
// Create new store
|
||||||
|
|||||||
@@ -198,6 +198,19 @@ func Test_Redis_Delete(t *testing.T) {
|
|||||||
require.Nil(t, keys)
|
require.Nil(t, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Redis_New_DisableStartupCheck(t *testing.T) {
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
store := New(Config{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 6390,
|
||||||
|
Addrs: []string{"127.0.0.1:6390"},
|
||||||
|
DisableStartupCheck: true,
|
||||||
|
})
|
||||||
|
require.NotNil(t, store)
|
||||||
|
_ = store.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_Redis_DeleteWithContext(t *testing.T) {
|
func Test_Redis_DeleteWithContext(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
key = "john"
|
key = "john"
|
||||||
|
|||||||
Reference in New Issue
Block a user