goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/database/timeout_test.go (about)

     1  package database
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/samber/lo"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	"gorm.io/driver/sqlite"
    14  	"gorm.io/gorm"
    15  	"goyave.dev/goyave/v5/config"
    16  )
    17  
    18  func prepareTimeoutTest(dbName string) *gorm.DB {
    19  	cfg := config.LoadDefault()
    20  	cfg.Set("app.debug", false)
    21  	cfg.Set("database.connection", "sqlite3_timeout_test")
    22  	cfg.Set("database.name", fmt.Sprintf("timeout_test_%s.db", dbName))
    23  	cfg.Set("database.options", "mode=memory")
    24  	cfg.Set("database.defaultReadQueryTimeout", 5)
    25  	cfg.Set("database.defaultWriteQueryTimeout", 5)
    26  	db, err := New(cfg, nil)
    27  	if err != nil {
    28  		panic(err)
    29  	}
    30  
    31  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    32  	if err := db.Session(&gorm.Session{NewDB: true, Context: ctx}).AutoMigrate(&TestUser{}); err != nil {
    33  		cancel()
    34  		panic(err)
    35  	}
    36  	cancel()
    37  
    38  	author := userGenerator()
    39  	if err := db.Create(author).Error; err != nil {
    40  		panic(err)
    41  	}
    42  	return db
    43  }
    44  
    45  func TestTimeoutPlugin(t *testing.T) {
    46  
    47  	RegisterDialect("sqlite3_timeout_test", "file:{name}?{options}", sqlite.Open)
    48  	t.Cleanup(func() {
    49  		mu.Lock()
    50  		delete(dialects, "sqlite3_timeout_test")
    51  		mu.Unlock()
    52  	})
    53  
    54  	t.Run("Callbacks", func(t *testing.T) {
    55  		db := prepareTimeoutTest(t.Name())
    56  
    57  		callbacks := db.Callback()
    58  
    59  		assert.NotNil(t, callbacks.Create().Get(timeoutCallbackBeforeName))
    60  		assert.NotNil(t, callbacks.Create().Get(timeoutCallbackAfterName))
    61  
    62  		assert.NotNil(t, callbacks.Query().Get(timeoutCallbackBeforeName))
    63  		assert.NotNil(t, callbacks.Query().Get(timeoutCallbackAfterName))
    64  
    65  		assert.NotNil(t, callbacks.Delete().Get(timeoutCallbackBeforeName))
    66  		assert.NotNil(t, callbacks.Delete().Get(timeoutCallbackAfterName))
    67  
    68  		assert.NotNil(t, callbacks.Update().Get(timeoutCallbackBeforeName))
    69  		assert.NotNil(t, callbacks.Update().Get(timeoutCallbackAfterName))
    70  
    71  		// assert.NotNil(t, callbacks.Row().Get(timeoutCallbackBeforeName))
    72  		// assert.NotNil(t, callbacks.Row().Get(timeoutCallbackAfterName))
    73  
    74  		assert.NotNil(t, callbacks.Raw().Get(timeoutCallbackBeforeName))
    75  		assert.NotNil(t, callbacks.Raw().Get(timeoutCallbackAfterName))
    76  	})
    77  
    78  	t.Run("timeout", func(t *testing.T) {
    79  		db := prepareTimeoutTest(t.Name())
    80  
    81  		// Generate a huge WHERE condition to artificially make the query very long
    82  		args := lo.RepeatBy(20000, func(index int) string {
    83  			return fmt.Sprintf("foobar_%d@example.org", index)
    84  		})
    85  
    86  		users := []*TestUser{}
    87  		res := db.Select("*").Where("email IN (?)", args).Find(&users)
    88  		require.Error(t, res.Error)
    89  		assert.Equal(t, "context deadline exceeded", res.Error.Error())
    90  	})
    91  
    92  	t.Run("re-use_statement", func(t *testing.T) {
    93  		db := prepareTimeoutTest(t.Name())
    94  
    95  		users := []*TestUser{}
    96  		db = db.Select("*").Where("email", "johndoe@example.org").Find(&users)
    97  		require.NoError(t, db.Error)
    98  		db = db.Select("*").Where("email", "johndoe@example.org").Find(&users)
    99  		require.NoError(t, db.Error)
   100  	})
   101  
   102  	t.Run("dont_override_predefined_context", func(t *testing.T) {
   103  		db := prepareTimeoutTest(t.Name())
   104  
   105  		// Generate a huge WHERE condition to artificially make the query very long
   106  		args := lo.RepeatBy(20000, func(index int) string {
   107  			return fmt.Sprintf("foobar_%d@example.org", index)
   108  		})
   109  
   110  		users := []*TestUser{}
   111  		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   112  		defer cancel()
   113  
   114  		// The context is replaced with a longer timeout so the query can be completed.
   115  		res := db.WithContext(ctx).Select("*").Where("email IN (?)", args).Find(&users)
   116  		require.NoError(t, res.Error)
   117  	})
   118  
   119  	t.Run("disabled", func(t *testing.T) {
   120  		cfg := config.LoadDefault()
   121  		cfg.Set("app.debug", false)
   122  		cfg.Set("database.connection", "sqlite3_timeout_test")
   123  		cfg.Set("database.name", "timeout_test_disabled.db")
   124  		cfg.Set("database.options", "mode=memory")
   125  		cfg.Set("database.defaultReadQueryTimeout", 0)
   126  		cfg.Set("database.defaultWriteQueryTimeout", 0)
   127  		db, err := New(cfg, nil)
   128  		if err != nil {
   129  			panic(err)
   130  		}
   131  
   132  		if err := db.AutoMigrate(&TestUser{}); err != nil {
   133  			panic(err)
   134  		}
   135  
   136  		// Generate a huge WHERE condition to artificially make the query very long
   137  		args := lo.RepeatBy(20000, func(index int) string {
   138  			return fmt.Sprintf("foobar_%d@example.org", index)
   139  		})
   140  
   141  		users := []*TestUser{}
   142  		res := db.Select("*").Where("email IN (?)", args).Find(&users)
   143  		require.NoError(t, res.Error)
   144  	})
   145  
   146  	t.Run("transaction_many_queries", func(t *testing.T) {
   147  		t.Cleanup(func() {
   148  			// The DB is not in memory here
   149  			if err := os.Remove("timeout_many_queries_test.db"); err != nil {
   150  				panic(err)
   151  			}
   152  		})
   153  		cfg := config.LoadDefault()
   154  		cfg.Set("app.debug", false)
   155  		cfg.Set("database.connection", "sqlite3_timeout_test")
   156  		cfg.Set("database.name", "timeout_many_queries_test.db")
   157  		cfg.Set("database.defaultReadQueryTimeout", 200)
   158  		cfg.Set("database.defaultWriteQueryTimeout", 200)
   159  		db, err := New(cfg, nil)
   160  		if err != nil {
   161  			panic(err)
   162  		}
   163  
   164  		ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
   165  		if err := db.WithContext(ctx).AutoMigrate(&TestUser{}); err != nil {
   166  			panic(err)
   167  		}
   168  		defer cancel()
   169  
   170  		author := userGenerator()
   171  		if err := db.Create(author).Error; err != nil {
   172  			panic(err)
   173  		}
   174  
   175  		// The timeout should be per query
   176  		// If we execute a lot of queries that take a cumulated time
   177  		// superior to the configured timeout, we should have no error.
   178  
   179  		// Generate a huge WHERE condition to artificially make the query long
   180  		args := lo.RepeatBy(1000, func(index int) string {
   181  			return fmt.Sprintf("foobar_%d@example.org", index)
   182  		})
   183  		err = db.Transaction(func(_ *gorm.DB) error {
   184  			for i := 0; i < 5000; i++ {
   185  				users := []*TestUser{}
   186  				res := db.Select("*").Where("email IN (?)", args).Find(&users)
   187  				if res.Error != nil {
   188  					return res.Error
   189  				}
   190  			}
   191  			return nil
   192  		})
   193  		require.NoError(t, err)
   194  	})
   195  }