diff --git a/.gitignore b/.gitignore index 4809a237..559aaa3d 100644 --- a/.gitignore +++ b/.gitignore @@ -199,3 +199,4 @@ pkg/**/*.html *.zstd web/.yarn/ docs/.yarn/ +coverage.txt diff --git a/build/Dockerfile b/build/Dockerfile index 7bca673d..98694b44 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION=1.22.1-alpine3.19 +ARG GOLANG_VERSION=1.22.2-alpine3.19 ARG NODE_VERSION=20-alpine3.19 ARG ALPINE_VERSION=3.19 diff --git a/build/Dockerfile.builder b/build/Dockerfile.builder index bde90d07..3f801116 100644 --- a/build/Dockerfile.builder +++ b/build/Dockerfile.builder @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION=1.22.1-alpine3.19 +ARG GOLANG_VERSION=1.22.2-alpine3.19 ARG BUILDKIT_VERSION=v0.12.4-rootless ARG ALPINE_VERSION=3.19 diff --git a/build/Dockerfile.debian b/build/Dockerfile.debian index 1bb0ba53..b8ad6109 100644 --- a/build/Dockerfile.debian +++ b/build/Dockerfile.debian @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION=1.22.1-bookworm +ARG GOLANG_VERSION=1.22.2-bookworm ARG NODE_VERSION=20-alpine3.19 ARG ALPINE_VERSION=3.19 ARG DEBIAN_VERSION=bookworm-slim diff --git a/go.mod b/go.mod index 985f8a59..3c93db47 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/caarlos0/env/v9 v9.0.0 github.com/casbin/casbin/v2 v2.84.1 github.com/casbin/gorm-adapter/v3 v3.21.0 - github.com/containers/podman/v5 v5.0.0 + github.com/containers/podman/v5 v5.0.1 github.com/deckarep/golang-set/v2 v2.6.0 github.com/distribution/distribution/v3 v3.0.0-alpha.1 github.com/distribution/reference v0.5.0 @@ -26,7 +26,6 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-git/go-git/v5 v5.11.0 github.com/go-playground/validator v9.31.0+incompatible - github.com/go-redsync/redsync/v4 v4.12.1 github.com/go-resty/resty/v2 v2.11.0 github.com/go-sql-driver/mysql v1.8.0 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -119,8 +118,8 @@ require ( github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect - github.com/containers/buildah v1.35.1 // indirect - github.com/containers/common v0.58.0 // indirect + github.com/containers/buildah v1.35.3 // indirect + github.com/containers/common v0.58.1 // indirect github.com/containers/image/v5 v5.30.0 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.1.9 // indirect diff --git a/go.sum b/go.sum index 0ba3a828..bcd0b143 100644 --- a/go.sum +++ b/go.sum @@ -282,18 +282,18 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containers/buildah v1.35.1 h1:m4TF6V8b06cS4jH9/t39PUsUIjzDQg/P14FLpwjr40Y= -github.com/containers/buildah v1.35.1/go.mod h1:vVSVUlTu8+99H5j43gBJscpkb/quZvdJg78+6X1HeTM= -github.com/containers/common v0.58.0 h1:iQuwMxDD4ubZ9s1tmgdsiaHxMU4TdVBpV6kctJc6Bk8= -github.com/containers/common v0.58.0/go.mod h1:l3vMqanJGj7tZ3W/i76gEJ128VXgFUO1tLaohJXPvdk= +github.com/containers/buildah v1.35.3 h1:Dn8Krwm2PemBNNOMwp7uiMK2e5cW2ZjTdLRzKM789pc= +github.com/containers/buildah v1.35.3/go.mod h1:kYi6vTHdbr1gnRo3B/RhTHsY2if/w398+/RvCxAXqkQ= +github.com/containers/common v0.58.1 h1:E1DN9Lr7kgMVQy7AXLv1CYQCiqnweklMiYWbf0KOnqY= +github.com/containers/common v0.58.1/go.mod h1:l3vMqanJGj7tZ3W/i76gEJ128VXgFUO1tLaohJXPvdk= github.com/containers/image/v5 v5.30.0 h1:CmHeSwI6W2kTRWnUsxATDFY5TEX4b58gPkaQcEyrLIA= github.com/containers/image/v5 v5.30.0/go.mod h1:gSD8MVOyqBspc0ynLsuiMR9qmt8UQ4jpVImjmK0uXfk= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.1.9 h1:2Csfba4jse85Raxk5HIyEk8OwZNjRvfkhEGijOjIdEM= github.com/containers/ocicrypt v1.1.9/go.mod h1:dTKx1918d8TDkxXvarscpNVY+lyPakPNFN4jwA9GBys= -github.com/containers/podman/v5 v5.0.0 h1:DVjeY4oTbI9qsxQfBTswerS8xvlAy0KywLN9yNc/MAA= -github.com/containers/podman/v5 v5.0.0/go.mod h1:lJXhiseM72otkIcp0sVDUn9aFyScHqmHmlarbboRX4I= +github.com/containers/podman/v5 v5.0.1 h1:40OJuoOJWxt3hk1j9J0jcwWIbWpjARxpzazYYtCDiNY= +github.com/containers/podman/v5 v5.0.1/go.mod h1:v09KUXu9AEFvzgiKSwkB49jcrVglXx2yi2qaPyaKNBE= github.com/containers/psgo v1.9.0 h1:eJ74jzSaCHnWt26OlKZROSyUyRcGDf+gYBdXnxrMW4g= github.com/containers/psgo v1.9.0/go.mod h1:0YoluUm43Mz2UnBIh1P+6V6NWcbpTL5uRtXyOcH0B5A= github.com/containers/storage v1.53.0 h1:VSES3C/u1pxjTJIXvLrSmyP7OBtDky04oGu07UvdTEA= @@ -476,14 +476,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= -github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4= -github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-redsync/redsync/v4 v4.12.1 h1:hCtdZ45DJxMxNdPiby5GlQwOKQmcka2587Y466qPqlA= -github.com/go-redsync/redsync/v4 v4.12.1/go.mod h1:sn72ojgeEhxUuRjrliK0NRrB0Zl6kOZ3BDvNN3P2jAY= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sigma/soft_delete v0.0.0-20231124084503-fb6a66078e2b h1:dP1itc+/9pJ/0ku3ntc8Rti0w+zuslJfOAnMNyv3N7I= @@ -559,8 +551,6 @@ github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= @@ -1078,8 +1068,6 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= -github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1206,8 +1194,6 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= -github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= diff --git a/pkg/cronjob/builder/builder.go b/pkg/cronjob/builder/builder.go index 22325e92..c2660cb1 100644 --- a/pkg/cronjob/builder/builder.go +++ b/pkg/cronjob/builder/builder.go @@ -72,17 +72,13 @@ func (r builderRunner) runner(ctx context.Context, tw timewheel.TimeWheel) { log.Error().Err(err).Msg("New locker failed") return } - lock, err := locker.Lock(context.Background(), consts.LockerCronjobBuilder, time.Second*30) + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + err = locker.AcquireWithRenew(ctx, consts.LockerCronjobBuilder, time.Second*3, time.Second*5) if err != nil { log.Error().Err(err).Msg("Cronjob builder get locker failed") return } - defer func() { - err := lock.Unlock() - if err != nil { - log.Error().Err(err).Msg("Migrate locker release failed") - } - }() ctx = log.Logger.WithContext(ctx) builderService := r.builderServiceFactory.New() diff --git a/pkg/dal/dal.go b/pkg/dal/dal.go index 39c37f01..9903aa78 100644 --- a/pkg/dal/dal.go +++ b/pkg/dal/dal.go @@ -73,16 +73,12 @@ func Initialize(config configs.Configuration) error { if err != nil { return err } - lock, err := locker.Lock(context.Background(), consts.LockerMigration, time.Second*30) + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + err = locker.AcquireWithRenew(ctx, consts.LockerMigration, time.Second*3, time.Second*5) if err != nil { return err } - defer func() { - err := lock.Unlock() - if err != nil { - log.Error().Err(err).Msg("Migrate locker release failed") - } - }() switch config.Database.Type { case enums.DatabaseMysql: @@ -165,7 +161,7 @@ func connectPostgres(config configs.Configuration) (string, error) { func connectSqlite3(config configs.Configuration) (string, error) { dbname := config.Database.Sqlite3.Path - db, err := gorm.Open(sqlite.Open(dbname), &gorm.Config{ + db, err := gorm.Open(sqlite.Open(dbname+"?_busy_timeout=10000"), &gorm.Config{ NowFunc: func() time.Time { return time.Now().UTC() }, diff --git a/pkg/dal/dao/locker.go b/pkg/dal/dao/locker.go index fc9b7ce2..f314b464 100644 --- a/pkg/dal/dao/locker.go +++ b/pkg/dal/dao/locker.go @@ -24,6 +24,7 @@ import ( "github.com/go-sigma/sigma/pkg/dal/models" "github.com/go-sigma/sigma/pkg/dal/query" + "github.com/go-sigma/sigma/pkg/modules/locker/definition" ) //go:generate mockgen -destination=mocks/locker.go -package=mocks github.com/go-sigma/sigma/pkg/dal/dao LockerService @@ -32,9 +33,11 @@ import ( // LockerService is the interface that provides methods to operate on locker model type LockerService interface { // Create creates a new work queue record in the database - Create(ctx context.Context, name string) error + Create(ctx context.Context, key, val string, expire int64) error // Delete get a locker record - Delete(ctx context.Context, name string) error + Delete(ctx context.Context, key, val string) error + // Renew renew a locker record + Renew(ctx context.Context, key, val string, expire int64) error } type lockerService struct { @@ -64,28 +67,52 @@ func (s *lockerServiceFactory) New(txs ...*query.Query) LockerService { } // Create creates a new work queue record in the database -func (s lockerService) Create(ctx context.Context, name string) error { - for i := 0; i < 6; i++ { - err := s.tx.Locker.WithContext(ctx).Create(&models.Locker{Name: name}) - if err == nil { - return nil +func (s lockerService) Create(ctx context.Context, key, value string, expire int64) error { + lock, err := s.tx.Locker.WithContext(ctx).Where(s.tx.Locker.Key.Eq(key)).First() + if err == nil { + if lock.Expire < time.Now().UnixMilli() { + _, err = s.tx.Locker.WithContext(ctx).Where(s.tx.Locker.Key.Eq(key)).Delete() + if err != nil { + return err + } + } else { + return fmt.Errorf("Locker %s already exists", key) } - if !errors.Is(err, gorm.ErrDuplicatedKey) { - return err - } - <-time.After(time.Second) } - return fmt.Errorf("cannot acquire locker for %s", name) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return s.tx.Locker.WithContext(ctx).Create(&models.Locker{Key: key, Value: value, Expire: expire}) } // Delete get a locker record -func (s lockerService) Delete(ctx context.Context, name string) error { - matched, err := s.tx.Locker.WithContext(ctx).Unscoped().Where(s.tx.Locker.Name.Eq(name)).Delete() - if err != nil { - return err - } - if matched.RowsAffected == 0 { - return gorm.ErrRecordNotFound +func (s lockerService) Delete(ctx context.Context, key, value string) error { + _, err := s.tx.Locker.WithContext(ctx).Unscoped().Where( + s.tx.Locker.Key.Eq(key), s.tx.Locker.Value.Eq(value)).Delete() + return err +} + +// Renew renew a locker record +func (s lockerService) Renew(ctx context.Context, key, value string, expire int64) error { + lock, err := s.tx.Locker.WithContext(ctx).Where(s.tx.Locker.Key.Eq(key)).First() + if err == nil { + if lock.Value != value { + return definition.ErrLockNotHeld + } + if lock.Expire < time.Now().UnixMilli() { + _, err = s.tx.Locker.WithContext(ctx).Where(s.tx.Locker.Key.Eq(key)).Delete() + if err != nil { + return err + } + return definition.ErrLockAlreadyExpired + } else { + _, err := s.tx.Locker.WithContext(ctx).Where(s.tx.Locker.Key.Eq(key)).UpdateColumns(map[string]any{ + query.Locker.Expire.ColumnName().String(): expire, + }) + if err != nil { + return err + } + } } - return nil + return err } diff --git a/pkg/dal/dao/mocks/locker.go b/pkg/dal/dao/mocks/locker.go index e07be4f2..49119b64 100644 --- a/pkg/dal/dao/mocks/locker.go +++ b/pkg/dal/dao/mocks/locker.go @@ -40,17 +40,17 @@ func (m *MockLockerService) EXPECT() *MockLockerServiceMockRecorder { } // Create mocks base method. -func (m *MockLockerService) Create(arg0 context.Context, arg1 string) error { +func (m *MockLockerService) Create(arg0 context.Context, arg1 string, arg2 int64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // Create indicates an expected call of Create. -func (mr *MockLockerServiceMockRecorder) Create(arg0, arg1 any) *gomock.Call { +func (mr *MockLockerServiceMockRecorder) Create(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockLockerService)(nil).Create), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockLockerService)(nil).Create), arg0, arg1, arg2) } // Delete mocks base method. @@ -66,3 +66,17 @@ func (mr *MockLockerServiceMockRecorder) Delete(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockLockerService)(nil).Delete), arg0, arg1) } + +// Renew mocks base method. +func (m *MockLockerService) Renew(arg0 context.Context, arg1 string, arg2 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Renew", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Renew indicates an expected call of Renew. +func (mr *MockLockerServiceMockRecorder) Renew(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Renew", reflect.TypeOf((*MockLockerService)(nil).Renew), arg0, arg1, arg2) +} diff --git a/pkg/dal/dao/workq.go b/pkg/dal/dao/workq.go index 56b326d5..7a16dcbe 100644 --- a/pkg/dal/dao/workq.go +++ b/pkg/dal/dao/workq.go @@ -32,7 +32,7 @@ type WorkQueueService interface { // Create creates a new work queue record in the database Create(ctx context.Context, workqObj *models.WorkQueue) error // Get get a work queue record - Get(ctx context.Context, topic string) (*models.WorkQueue, error) + Get(ctx context.Context, topic enums.Daemon) (*models.WorkQueue, error) // UpdateStatus update a work queue record status UpdateStatus(ctx context.Context, id int64, version, newVersion string, times int, status enums.TaskCommonStatus) error } @@ -69,7 +69,7 @@ func (s workQueueService) Create(ctx context.Context, workqObj *models.WorkQueue } // Get get a work queue record -func (s workQueueService) Get(ctx context.Context, topic string) (*models.WorkQueue, error) { +func (s workQueueService) Get(ctx context.Context, topic enums.Daemon) (*models.WorkQueue, error) { return s.tx.WorkQueue.WithContext(ctx).Where( s.tx.WorkQueue.Status.Eq(enums.TaskCommonStatusPending), s.tx.WorkQueue.Topic.Eq(topic), diff --git a/pkg/dal/dao/workq_test.go b/pkg/dal/dao/workq_test.go index 5184cdc7..1339b968 100644 --- a/pkg/dal/dao/workq_test.go +++ b/pkg/dal/dao/workq_test.go @@ -72,7 +72,7 @@ func TestWorkQueueService(t *testing.T) { err = workqService.UpdateStatus(ctx, workqObj.ID, "version", "newVersion", 1, enums.TaskCommonStatusPending) assert.NoError(t, err) - workqNewObj, err := workqService.Get(ctx, enums.DaemonGc.String()) + workqNewObj, err := workqService.Get(ctx, enums.DaemonGc) assert.NoError(t, err) assert.Equal(t, workqObj.ID, workqNewObj.ID) assert.Equal(t, workqObj.Topic, workqNewObj.Topic) diff --git a/pkg/dal/migrations/mysql/0003_upgrade.down.sql b/pkg/dal/migrations/mysql/0003_upgrade.down.sql new file mode 100644 index 00000000..4b6cf70f --- /dev/null +++ b/pkg/dal/migrations/mysql/0003_upgrade.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `lockers`; + diff --git a/pkg/dal/migrations/mysql/0003_upgrade.up.sql b/pkg/dal/migrations/mysql/0003_upgrade.up.sql new file mode 100644 index 00000000..039242ed --- /dev/null +++ b/pkg/dal/migrations/mysql/0003_upgrade.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `lockers` ( + `id` bigint AUTO_INCREMENT PRIMARY KEY, + `key` varchar(256) NOT NULL, + `value` varchar(256) NOT NULL, + `expire` bigint NOT NULL DEFAULT 0, + `created_at` bigint NOT NULL DEFAULT (UNIX_TIMESTAMP (CURRENT_TIMESTAMP()) * 1000), + `updated_at` bigint NOT NULL DEFAULT (UNIX_TIMESTAMP (CURRENT_TIMESTAMP()) * 1000), + `deleted_at` bigint NOT NULL DEFAULT 0, + CONSTRAINT `idx_lockers_key` UNIQUE (`key`, `deleted_at`) +); + diff --git a/pkg/dal/migrations/postgresql/0003_upgrade.down.sql b/pkg/dal/migrations/postgresql/0003_upgrade.down.sql new file mode 100644 index 00000000..268d8a56 --- /dev/null +++ b/pkg/dal/migrations/postgresql/0003_upgrade.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS "lockers"; + diff --git a/pkg/dal/migrations/postgresql/0003_upgrade.up.sql b/pkg/dal/migrations/postgresql/0003_upgrade.up.sql new file mode 100644 index 00000000..f35d58a2 --- /dev/null +++ b/pkg/dal/migrations/postgresql/0003_upgrade.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS "lockers" ( + "id" bigserial PRIMARY KEY, + "key" varchar(256) NOT NULL, + "value" varchar(256) NOT NULL, + "expire" bigint NOT NULL DEFAULT 0, + "created_at" bigint NOT NULL DEFAULT ((EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::bigint), + "updated_at" bigint NOT NULL DEFAULT ((EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::bigint), + "deleted_at" bigint NOT NULL DEFAULT 0, + CONSTRAINT "idx_lockers_key" UNIQUE ("key", "deleted_at") +); + diff --git a/pkg/dal/migrations/sqlite3/0003_upgrade.down.sql b/pkg/dal/migrations/sqlite3/0003_upgrade.down.sql new file mode 100644 index 00000000..4b6cf70f --- /dev/null +++ b/pkg/dal/migrations/sqlite3/0003_upgrade.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `lockers`; + diff --git a/pkg/dal/migrations/sqlite3/0003_upgrade.up.sql b/pkg/dal/migrations/sqlite3/0003_upgrade.up.sql new file mode 100644 index 00000000..858ead58 --- /dev/null +++ b/pkg/dal/migrations/sqlite3/0003_upgrade.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `lockers` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `key` varchar(256) NOT NULL, + `value` varchar(256) NOT NULL, + `expire` integer NOT NULL DEFAULT 0, + `created_at` integer NOT NULL DEFAULT (unixepoch () * 1000), + `updated_at` integer NOT NULL DEFAULT (unixepoch () * 1000), + `deleted_at` integer NOT NULL DEFAULT 0, + CONSTRAINT `idx_lockers_key` UNIQUE (`key`, `deleted_at`) +); + diff --git a/pkg/dal/models/locker.go b/pkg/dal/models/locker.go index aa08dc78..926a5ebb 100644 --- a/pkg/dal/models/locker.go +++ b/pkg/dal/models/locker.go @@ -25,5 +25,7 @@ type Locker struct { DeletedAt soft_delete.DeletedAt `gorm:"softDelete:milli"` ID int64 `gorm:"primaryKey"` - Name string `gorm:"uniqueIndex,size:256"` + Key string + Expire int64 + Value string } diff --git a/pkg/dal/query/lockers.gen.go b/pkg/dal/query/lockers.gen.go index 23a7557a..7cf483b7 100644 --- a/pkg/dal/query/lockers.gen.go +++ b/pkg/dal/query/lockers.gen.go @@ -31,7 +31,9 @@ func newLocker(db *gorm.DB, opts ...gen.DOOption) locker { _locker.UpdatedAt = field.NewInt64(tableName, "updated_at") _locker.DeletedAt = field.NewUint64(tableName, "deleted_at") _locker.ID = field.NewInt64(tableName, "id") - _locker.Name = field.NewString(tableName, "name") + _locker.Key = field.NewString(tableName, "key") + _locker.Expire = field.NewInt64(tableName, "expire") + _locker.Value = field.NewString(tableName, "value") _locker.fillFieldMap() @@ -46,7 +48,9 @@ type locker struct { UpdatedAt field.Int64 DeletedAt field.Uint64 ID field.Int64 - Name field.String + Key field.String + Expire field.Int64 + Value field.String fieldMap map[string]field.Expr } @@ -67,7 +71,9 @@ func (l *locker) updateTableName(table string) *locker { l.UpdatedAt = field.NewInt64(table, "updated_at") l.DeletedAt = field.NewUint64(table, "deleted_at") l.ID = field.NewInt64(table, "id") - l.Name = field.NewString(table, "name") + l.Key = field.NewString(table, "key") + l.Expire = field.NewInt64(table, "expire") + l.Value = field.NewString(table, "value") l.fillFieldMap() @@ -92,12 +98,14 @@ func (l *locker) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (l *locker) fillFieldMap() { - l.fieldMap = make(map[string]field.Expr, 5) + l.fieldMap = make(map[string]field.Expr, 7) l.fieldMap["created_at"] = l.CreatedAt l.fieldMap["updated_at"] = l.UpdatedAt l.fieldMap["deleted_at"] = l.DeletedAt l.fieldMap["id"] = l.ID - l.fieldMap["name"] = l.Name + l.fieldMap["key"] = l.Key + l.fieldMap["expire"] = l.Expire + l.fieldMap["value"] = l.Value } func (l locker) clone(db *gorm.DB) locker { diff --git a/pkg/dal/query/work_queues.gen.go b/pkg/dal/query/work_queues.gen.go index 7ba9fbb2..26beef96 100644 --- a/pkg/dal/query/work_queues.gen.go +++ b/pkg/dal/query/work_queues.gen.go @@ -31,7 +31,7 @@ func newWorkQueue(db *gorm.DB, opts ...gen.DOOption) workQueue { _workQueue.UpdatedAt = field.NewInt64(tableName, "updated_at") _workQueue.DeletedAt = field.NewUint64(tableName, "deleted_at") _workQueue.ID = field.NewInt64(tableName, "id") - _workQueue.Topic = field.NewString(tableName, "topic") + _workQueue.Topic = field.NewField(tableName, "topic") _workQueue.Payload = field.NewBytes(tableName, "payload") _workQueue.Times = field.NewInt(tableName, "times") _workQueue.Version = field.NewString(tableName, "version") @@ -50,7 +50,7 @@ type workQueue struct { UpdatedAt field.Int64 DeletedAt field.Uint64 ID field.Int64 - Topic field.String + Topic field.Field Payload field.Bytes Times field.Int Version field.String @@ -75,7 +75,7 @@ func (w *workQueue) updateTableName(table string) *workQueue { w.UpdatedAt = field.NewInt64(table, "updated_at") w.DeletedAt = field.NewUint64(table, "deleted_at") w.ID = field.NewInt64(table, "id") - w.Topic = field.NewString(table, "topic") + w.Topic = field.NewField(table, "topic") w.Payload = field.NewBytes(table, "payload") w.Times = field.NewInt(table, "times") w.Version = field.NewString(table, "version") diff --git a/pkg/dal/redis/redis.go b/pkg/dal/redis/redis.go new file mode 100644 index 00000000..637d87b4 --- /dev/null +++ b/pkg/dal/redis/redis.go @@ -0,0 +1,49 @@ +// Copyright 2024 sigma +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redis + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" + + "github.com/go-sigma/sigma/pkg/configs" + "github.com/go-sigma/sigma/pkg/types/enums" +) + +// Client ... +var Client redis.UniversalClient + +// Initialize init redis +func Initialize(ctx context.Context, config configs.Configuration) error { + if config.Redis.Type == enums.RedisTypeNone { + return nil + } + redisOpt, err := redis.ParseURL(config.Redis.Url) + if err != nil { + return fmt.Errorf("redis.ParseURL error: %v", err) + } + redisCli := redis.NewClient(redisOpt) + res, err := redisCli.Ping(ctx).Result() + if err != nil { + return fmt.Errorf("redis ping error: %v", err) + } + if res != "PONG" { + return fmt.Errorf("redis ping should got PONG, real: %s", res) + } + Client = redisCli + return nil +} diff --git a/pkg/dal/redis/redis_test.go b/pkg/dal/redis/redis_test.go new file mode 100644 index 00000000..6f206157 --- /dev/null +++ b/pkg/dal/redis/redis_test.go @@ -0,0 +1,53 @@ +// Copyright 2024 sigma +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redis + +import ( + "context" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + + "github.com/go-sigma/sigma/pkg/configs" + "github.com/go-sigma/sigma/pkg/types/enums" +) + +func TestRedis(t *testing.T) { + err := Initialize(context.Background(), configs.Configuration{ + Redis: configs.ConfigurationRedis{ + Type: enums.RedisTypeNone, + Url: "", + }, + }) + assert.NoError(t, err) + assert.Nil(t, Client) + + err = Initialize(context.Background(), configs.Configuration{ + Redis: configs.ConfigurationRedis{ + Type: enums.RedisTypeExternal, + Url: miniredis.RunT(t).Addr(), + }, + }) + assert.Error(t, err) + + err = Initialize(context.Background(), configs.Configuration{ + Redis: configs.ConfigurationRedis{ + Type: enums.RedisTypeExternal, + Url: "redis://" + miniredis.RunT(t).Addr(), + }, + }) + assert.NoError(t, err) +} diff --git a/pkg/inits/baseimage.go b/pkg/inits/baseimage.go index e3d897ef..96b0f8a8 100644 --- a/pkg/inits/baseimage.go +++ b/pkg/inits/baseimage.go @@ -56,16 +56,12 @@ func initBaseimage(config configs.Configuration) error { if err != nil { return err } - lock, err := locker.Lock(context.Background(), consts.LockerBaseimage, time.Second*30) + ctx, ctxCancel := context.WithCancel(context.Background()) + defer ctxCancel() + err = locker.AcquireWithRenew(ctx, consts.LockerBaseimage, time.Second*3, time.Second*5) if err != nil { return err } - defer func() { - err := lock.Unlock() - if err != nil { - log.Error().Err(err).Msg("Initialize baseimage failed") - } - }() err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if !d.IsDir() && strings.HasSuffix(path, ".tar") { diff --git a/pkg/modules/locker/database/database.go b/pkg/modules/locker/database/database.go index 86eb709f..e42f9958 100644 --- a/pkg/modules/locker/database/database.go +++ b/pkg/modules/locker/database/database.go @@ -17,6 +17,8 @@ package database import ( "context" "errors" + "fmt" + "math/rand" // nolint: gosec "time" "github.com/rs/zerolog/log" @@ -24,6 +26,7 @@ import ( "github.com/go-sigma/sigma/pkg/configs" "github.com/go-sigma/sigma/pkg/dal/dao" + "github.com/go-sigma/sigma/pkg/dal/query" "github.com/go-sigma/sigma/pkg/modules/locker/definition" ) @@ -38,40 +41,112 @@ func New(config configs.Configuration) (definition.Locker, error) { } type lock struct { - name string - release bool + key, value string + expire time.Duration lockerServiceFactory dao.LockerServiceFactory } // Lock ... -func (l lockerDatabase) Lock(ctx context.Context, name string, expire time.Duration) (definition.Lock, error) { - err := l.lockerServiceFactory.New().Create(ctx, name) - if err != nil { - return nil, err - } - locker := &lock{ - name: name, - lockerServiceFactory: l.lockerServiceFactory, +func (l lockerDatabase) Acquire(ctx context.Context, key string, expire, waitTimeout time.Duration) (definition.Lock, error) { + if expire < 100*time.Millisecond { + return nil, definition.ErrLockTooShort } - time.AfterFunc(expire, func() { - err = locker.Unlock() + ddlCtx, cancel := context.WithTimeout(ctx, waitTimeout) + defer cancel() + ticker := time.NewTicker(time.Duration(100) * time.Millisecond) + defer func() { + ticker.Stop() + }() + for { + select { + case <-ddlCtx.Done(): + return nil, ddlCtx.Err() + case <-ticker.C: + } + val := fmt.Sprintf("%d-%d", rand.Int(), time.Now().Nanosecond()) // nolint: gosec + err := query.Q.Transaction(func(tx *query.Query) error { + lockerService := l.lockerServiceFactory.New(tx) + return lockerService.Create(ctx, key, val, time.Now().Add(expire).UnixMilli()) + }) if err != nil { - log.Error().Err(err).Msgf("Delete locker(%s) failed", name) + log.Error().Err(err).Msg("Create locker failed, wait for retry") + continue } - }) - return locker, nil + return &lock{ + key: key, + value: val, + expire: expire, + lockerServiceFactory: l.lockerServiceFactory, + }, nil + } } -// Unlock ... -func (l *lock) Unlock() error { - if !l.release { - err := l.lockerServiceFactory.New().Delete(context.Background(), l.name) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - log.Error().Err(err).Msgf("Delete locker(%s) failed", l.name) - return err +// AcquireWithRenew acquire lock with renew the lock +func (l lockerDatabase) AcquireWithRenew(ctx context.Context, key string, expire, waitTimeout time.Duration) error { + lock, err := l.Acquire(ctx, key, expire, waitTimeout) + if err != nil { + return err + } + + go func() { + ticker := time.NewTicker(time.Duration(100) * time.Millisecond) + defer func() { + ticker.Stop() + }() + for { + select { + case <-ctx.Done(): + err := lock.Unlock(context.Background()) // should always release the locker + if err != nil { + log.Error().Err(err).Msg("release lock failed") + } + return + case <-ticker.C: + } + if err := lock.Renew(ctx, expire); err != nil { + return + } + } + }() + return nil +} + +// Renew ... +func (l lock) Renew(ctx context.Context, ttls ...time.Duration) error { + var expire time.Duration + if len(ttls) == 0 { + expire = l.expire + } else { + expire = ttls[0] + } + if expire < definition.MinLockExpire { + return definition.ErrLockTooShort + } + err := query.Q.Transaction(func(tx *query.Query) error { + lockerService := l.lockerServiceFactory.New(tx) + return lockerService.Renew(ctx, l.key, l.value, time.Now().Add(expire).UnixMilli()) + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) || + errors.Is(err, definition.ErrLockAlreadyExpired) { + log.Error().Err(err).Msg("Locker already expired") + return definition.ErrLockAlreadyExpired } - l.release = true - return nil + if errors.Is(err, definition.ErrLockNotHeld) { + log.Error().Err(err).Msg("Locker not held") + return definition.ErrLockNotHeld + } + log.Error().Err(err).Msg("Renew locker failed") + return fmt.Errorf("Renew locker failed") } return nil } + +// Unlock ... +func (l *lock) Unlock(ctx context.Context) error { + err := query.Q.Transaction(func(tx *query.Query) error { + lockerService := l.lockerServiceFactory.New(tx) + return lockerService.Delete(ctx, l.key, l.value) + }) + return err +} diff --git a/pkg/modules/locker/database/database_test.go b/pkg/modules/locker/database/database_test.go new file mode 100644 index 00000000..5fd6d6a3 --- /dev/null +++ b/pkg/modules/locker/database/database_test.go @@ -0,0 +1,108 @@ +// Copyright 2024 sigma +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package database_test + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/go-sigma/sigma/pkg/configs" + "github.com/go-sigma/sigma/pkg/dal" + "github.com/go-sigma/sigma/pkg/modules/locker/database" + "github.com/go-sigma/sigma/pkg/tests" +) + +func TestDatabaseAcquire(t *testing.T) { + assert.NoError(t, tests.Initialize(t)) + assert.NoError(t, tests.DB.Init()) + defer func() { + conn, err := dal.DB.DB() + assert.NoError(t, err) + assert.NoError(t, conn.Close()) + assert.NoError(t, tests.DB.DeInit()) + }() + + config := configs.Configuration{} + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + c, err := database.New(config) + assert.NoError(t, err) + + const key = "test-redis-lock" + var concurrency uint64 = 500 + + var wg sync.WaitGroup + var cnt uint64 = 0 + for i := uint64(0); i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + l, err := c.Acquire(ctx, key, time.Second*1, time.Second*3) + assert.Equal(t, true, err == nil || errors.Is(err, context.DeadlineExceeded)) + if l != nil { + <-time.After(time.Millisecond * 100) + defer l.Unlock(ctx) // nolint: errcheck + atomic.AddUint64(&cnt, 1) + } + }() + } + wg.Wait() + assert.True(t, true, concurrency > cnt && cnt > 1) +} + +func TestDatabaseAcquireWithRenew(t *testing.T) { + assert.NoError(t, tests.Initialize(t)) + assert.NoError(t, tests.DB.Init()) + defer func() { + conn, err := dal.DB.DB() + assert.NoError(t, err) + assert.NoError(t, conn.Close()) + assert.NoError(t, tests.DB.DeInit()) + }() + + config := configs.Configuration{} + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + c, err := database.New(config) + assert.NoError(t, err) + + const key = "test-redis-lock" + var concurrency uint64 = 500 + + var wg sync.WaitGroup + var cnt uint64 = 0 + for i := uint64(0); i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := c.AcquireWithRenew(ctx, key, time.Second*1, time.Second*3) + if errors.Is(err, context.DeadlineExceeded) { + atomic.AddUint64(&cnt, 1) + } + }() + } + wg.Wait() + assert.Equal(t, true, cnt >= 1) +} diff --git a/pkg/modules/locker/definition/definition.go b/pkg/modules/locker/definition/definition.go index 869a1f13..bd6cede8 100644 --- a/pkg/modules/locker/definition/definition.go +++ b/pkg/modules/locker/definition/definition.go @@ -16,17 +16,36 @@ package definition import ( "context" + "errors" "time" ) -// Lock ... +const ( + // MinLockExpire ... + MinLockExpire = 100 * time.Millisecond +) + +var ( + // ErrLockNotHeld is returned when trying to release an inactive lock. + ErrLockNotHeld = errors.New("locker not held") + // ErrLockTooShort expire should longer than 100ms + ErrLockTooShort = errors.New("locker expire is too short") + // ErrLockAlreadyExpired lock already expired + ErrLockAlreadyExpired = errors.New("locker already expired") +) + +// Lock lock interface type Lock interface { // Unlock ... - Unlock() error + Unlock(ctx context.Context) error + // Renew ... + Renew(ctx context.Context, ttls ...time.Duration) error } -// Locker ... +// Locker locker interface type Locker interface { - // Lock ... - Lock(ctx context.Context, name string, expire time.Duration) (Lock, error) + // Acquire ... + Acquire(ctx context.Context, key string, expire, waitTimeout time.Duration) (Lock, error) + // AcquireWithRenew acquire lock with renew the lock + AcquireWithRenew(ctx context.Context, key string, expire, waitTimeout time.Duration) error } diff --git a/pkg/modules/locker/redis/redis.go b/pkg/modules/locker/redis/redis.go index 921bc071..6eaa9c05 100644 --- a/pkg/modules/locker/redis/redis.go +++ b/pkg/modules/locker/redis/redis.go @@ -16,45 +16,142 @@ package redis import ( "context" + "fmt" + "math/rand" // nolint: gosec "time" - "github.com/go-redsync/redsync/v4" - "github.com/go-redsync/redsync/v4/redis/goredis/v9" "github.com/redis/go-redis/v9" + "github.com/rs/zerolog/log" "github.com/go-sigma/sigma/pkg/configs" - "github.com/go-sigma/sigma/pkg/consts" + rds "github.com/go-sigma/sigma/pkg/dal/redis" "github.com/go-sigma/sigma/pkg/modules/locker/definition" + "github.com/go-sigma/sigma/pkg/types/enums" +) + +var ( + luaRelease = redis.NewScript(`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`) + luaRenew = redis.NewScript(`if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end`) ) type lockerRedis struct { - redsync *redsync.Redsync + redisCli redis.UniversalClient } func New(config configs.Configuration) (definition.Locker, error) { - redisOpt, err := redis.ParseURL(config.Redis.Url) - if err != nil { - return nil, err + if config.Redis.Type != enums.RedisTypeExternal { + return nil, fmt.Errorf("redislock: please check redis configuration, it should be external") } return &lockerRedis{ - redsync: redsync.New(goredis.NewPool(redis.NewClient(redisOpt))), + redisCli: rds.Client, }, nil } type lock struct { - mutex *redsync.Mutex + redisCli redis.UniversalClient + key, value string + expire time.Duration +} + +func (l lockerRedis) Acquire(ctx context.Context, key string, expire, waitTimeout time.Duration) (definition.Lock, error) { + if expire < definition.MinLockExpire { + return nil, definition.ErrLockTooShort + } + ddlCtx, cancel := context.WithTimeout(ctx, waitTimeout) + defer cancel() + val := fmt.Sprintf("%d-%d", rand.Int(), time.Now().Nanosecond()) // nolint: gosec + ticker := time.NewTicker(time.Duration(100) * time.Millisecond) + defer func() { + ticker.Stop() + }() + for { + select { + case <-ddlCtx.Done(): + return nil, ddlCtx.Err() + case <-ticker.C: + ok, err := l.redisCli.SetNX(ddlCtx, key, val, expire).Result() + if err != nil { + return nil, err + } + if ok { + return &lock{ + redisCli: l.redisCli, + key: key, + value: val, + expire: expire, + }, nil + } + } + } +} + +// AcquireWithRenew acquire lock with renew the lock +func (l lockerRedis) AcquireWithRenew(ctx context.Context, key string, expire, waitTimeout time.Duration) error { + lock, err := l.Acquire(ctx, key, expire, waitTimeout) + if err != nil { + return err + } + + var tick = time.Duration(100) * time.Millisecond + + go func() { + ticker := time.NewTicker(tick) + defer func() { + ticker.Stop() + }() + for { + select { + case <-ctx.Done(): + err := lock.Unlock(context.Background()) // should always release the locker + if err != nil { + log.Error().Err(err).Msg("release lock failed") + } + return + case <-ticker.C: + } + if err := lock.Renew(ctx, expire); err != nil { + return + } + } + }() + return nil } -func (l lockerRedis) Lock(ctx context.Context, name string, expire time.Duration) (definition.Lock, error) { - var opts = []redsync.Option{redsync.WithRetryDelay(consts.LockerRetryDelay), redsync.WithTries(consts.LockerRetryMaxTimes)} - if expire != 0 { - opts = append(opts, redsync.WithExpiry(expire)) +// Renew renew the lock +func (l lock) Renew(ctx context.Context, ttls ...time.Duration) error { + var expire time.Duration + if len(ttls) == 0 { + expire = l.expire + } else { + expire = ttls[0] + } + if expire < definition.MinLockExpire { + return definition.ErrLockTooShort + } + res, err := luaRenew.Run(ctx, l.redisCli, []string{l.key}, l.value, int64(expire/time.Second)).Result() + if err == redis.Nil { + return definition.ErrLockNotHeld + } else if err != nil { + return err + } + + if i, ok := res.(int64); !ok || i != 1 { + return definition.ErrLockNotHeld } - mutex := l.redsync.NewMutex(consts.LockerMigration, opts...) - return &lock{mutex: mutex}, nil + return nil } -func (l lock) Unlock() error { - _, err := l.mutex.Unlock() - return err +// Unlock unlock the lock +func (l lock) Unlock(ctx context.Context) error { + res, err := luaRelease.Run(ctx, l.redisCli, []string{l.key}, l.value).Result() + if err == redis.Nil { + return definition.ErrLockNotHeld + } else if err != nil { + return err + } + + if i, ok := res.(int64); !ok || i != 1 { + return definition.ErrLockNotHeld + } + return nil } diff --git a/pkg/modules/locker/redis/redis_test.go b/pkg/modules/locker/redis/redis_test.go new file mode 100644 index 00000000..6e6215bb --- /dev/null +++ b/pkg/modules/locker/redis/redis_test.go @@ -0,0 +1,108 @@ +// Copyright 2024 sigma +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redis + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + + "github.com/go-sigma/sigma/pkg/configs" + "github.com/go-sigma/sigma/pkg/dal/redis" + "github.com/go-sigma/sigma/pkg/types/enums" +) + +func TestRedisAcquire(t *testing.T) { + miniRedis := miniredis.RunT(t) + config := configs.Configuration{ + Redis: configs.ConfigurationRedis{ + Type: enums.RedisTypeExternal, + Url: "redis://" + miniRedis.Addr(), + }, + } + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + err := redis.Initialize(ctx, config) + assert.NoError(t, err) + + c, err := New(config) + assert.NoError(t, err) + + const key = "test-redis-lock" + var concurrency uint64 = 500 + + var wg sync.WaitGroup + var cnt uint64 = 0 + for i := uint64(0); i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + l, err := c.Acquire(ctx, key, time.Second*1, time.Second*3) + assert.Equal(t, true, err == nil || errors.Is(err, context.DeadlineExceeded)) + if l != nil { + <-time.After(time.Millisecond * 100) + defer l.Unlock(ctx) // nolint: errcheck + } + atomic.AddUint64(&cnt, 1) + }() + } + wg.Wait() + assert.True(t, true, concurrency > cnt && cnt > 1) +} + +func TestRedisAcquireWithRenew(t *testing.T) { + miniRedis := miniredis.RunT(t) + config := configs.Configuration{ + Redis: configs.ConfigurationRedis{ + Type: enums.RedisTypeExternal, + Url: "redis://" + miniRedis.Addr(), + }, + } + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + err := redis.Initialize(ctx, config) + assert.NoError(t, err) + + c, err := New(config) + assert.NoError(t, err) + + const key = "test-redis-lock" + var concurrency uint64 = 500 + + var wg sync.WaitGroup + var cnt uint64 = 0 + for i := uint64(0); i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := c.AcquireWithRenew(ctx, key, time.Second*1, time.Second*3) + if errors.Is(err, context.DeadlineExceeded) { + atomic.AddUint64(&cnt, 1) + } + }() + } + wg.Wait() + assert.Equal(t, cnt, concurrency-1) +} diff --git a/pkg/modules/workq/database/consumer.go b/pkg/modules/workq/database/consumer.go index eafb94b8..73a0f2c6 100644 --- a/pkg/modules/workq/database/consumer.go +++ b/pkg/modules/workq/database/consumer.go @@ -68,7 +68,7 @@ func (h *consumerHandler) consume(topic enums.Daemon) error { workQueueService := dao.NewWorkQueueServiceFactory().New() // daoCtx := log.Logger.WithContext(context.Background()) daoCtx := context.Background() - wq, err := workQueueService.Get(daoCtx, topic.String()) + wq, err := workQueueService.Get(daoCtx, topic) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Trace().Err(err).Msgf("None task in topic(%s)", topic) diff --git a/pkg/modules/workq/workq.go b/pkg/modules/workq/workq.go index 8795b68c..d0e74029 100644 --- a/pkg/modules/workq/workq.go +++ b/pkg/modules/workq/workq.go @@ -39,7 +39,6 @@ var ProducerClient definition.WorkQueueProducer // Initialize ... func Initialize(config configs.Configuration) error { - fmt.Println(42, config.WorkQueue.Type) var err error switch config.WorkQueue.Type { case enums.WorkQueueTypeDatabase: diff --git a/web/package.json b/web/package.json index 81e9fbd9..0b5daafa 100644 --- a/web/package.json +++ b/web/package.json @@ -17,14 +17,14 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/line-clamp": "^0.4.4", - "@tailwindcss/typography": "^0.5.10", + "@tailwindcss/typography": "^0.5.12", "axios": "^1.6.8", "bytemd": "^1.21.0", "cron-parser": "^4.9.0", "csstype": "^3.1.3", "dayjs": "^1.11.10", "flowbite": "^2.3.0", - "flowbite-react": "^0.7.5", + "flowbite-react": "^0.7.8", "human-format": "^1.2.0", "lodash": "^4.17.21", "monaco-editor": "^0.47.0", @@ -41,19 +41,19 @@ "xterm-addon-fit": "^0.8.0" }, "devDependencies": { - "@types/node": "^20.11.30", - "@types/react": "^18.2.69", - "@types/react-dom": "^18.2.22", + "@types/node": "^20.12.4", + "@types/react": "^18.2.74", + "@types/react-dom": "^18.2.24", "@vitejs/plugin-react-swc": "^3.6.0", "autoprefixer": "^10.4.19", - "cssnano": "^6.1.1", + "cssnano": "^6.1.2", "json-server": "^1.0.0-alpha.23", "postcss": "^8.4.38", "postcss-import": "^16.1.0", - "postcss-nesting": "^12.1.0", - "tailwindcss": "^3.4.1", - "typescript": "^5.4.3", - "vite": "^5.2.4" + "postcss-nesting": "^12.1.1", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.4", + "vite": "^5.2.8" }, "packageManager": "yarn@4.1.1" } diff --git a/web/yarn.lock b/web/yarn.lock index d492d1b8..0df94732 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.23.7": +"@babel/runtime@npm:^7.1.2": version: 7.23.9 resolution: "@babel/runtime@npm:7.23.9" dependencies: @@ -21,6 +21,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.24.0": + version: 7.24.4 + resolution: "@babel/runtime@npm:7.24.4" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/785aff96a3aa8ff97f90958e1e8a7b1d47f793b204b47c6455eaadc3f694f48c97cd5c0a921fe3596d818e71f18106610a164fb0f1c71fd68c622a58269d537c + languageName: node + linkType: hard + "@bytemd/plugin-gfm@npm:^1.21.0": version: 1.21.0 resolution: "@bytemd/plugin-gfm@npm:1.21.0" @@ -52,12 +61,12 @@ __metadata: languageName: node linkType: hard -"@csstools/selector-specificity@npm:^3.0.2": - version: 3.0.2 - resolution: "@csstools/selector-specificity@npm:3.0.2" +"@csstools/selector-specificity@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/selector-specificity@npm:3.0.3" peerDependencies: postcss-selector-parser: ^6.0.13 - checksum: 10c0/d0c7dae2f1e9536e3e17f00467320a704f3208c76283c29c57fd69d4b83dcf6d062f492ed687c5ffd5f47fada9f0657c2efc89ea18fd4b038f757669553e0095 + checksum: 10c0/e4f0355165882ddde8bd4a2f0252868150e67b9fae927fd2d94a91cee31e438e7041059f20b9c755a93b0bd8e527a9f78b01168fe67b3539be32091240aa63bf languageName: node linkType: hard @@ -222,7 +231,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.0.0": +"@floating-ui/core@npm:1.6.0, @floating-ui/core@npm:^1.0.0": version: 1.6.0 resolution: "@floating-ui/core@npm:1.6.0" dependencies: @@ -241,7 +250,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.0.8": +"@floating-ui/react-dom@npm:^2.0.0": version: 2.0.8 resolution: "@floating-ui/react-dom@npm:2.0.8" dependencies: @@ -253,17 +262,17 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react@npm:^0.26.2": - version: 0.26.9 - resolution: "@floating-ui/react@npm:0.26.9" +"@floating-ui/react@npm:0.26.10": + version: 0.26.10 + resolution: "@floating-ui/react@npm:0.26.10" dependencies: - "@floating-ui/react-dom": "npm:^2.0.8" - "@floating-ui/utils": "npm:^0.2.1" - tabbable: "npm:^6.0.1" + "@floating-ui/react-dom": "npm:^2.0.0" + "@floating-ui/utils": "npm:^0.2.0" + tabbable: "npm:^6.0.0" peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 10c0/769a6bc33c4fa6c8c38b2e1c91622854e5e8fdf39cb92b0998a7ca099dc831a551a005cd0aec7e98edf9bfed4f697397335f03034b5f41a0f4fb17c97fce6d20 + checksum: 10c0/6d960dd5c681aebe98103437f5289d4f4fae7dc49e098051cef438a986be563aa791157416c843d063208c434f578a1ee15402f47b7c2ea6240321fa023d38bf languageName: node linkType: hard @@ -703,9 +712,9 @@ __metadata: languageName: node linkType: hard -"@tailwindcss/typography@npm:^0.5.10": - version: 0.5.10 - resolution: "@tailwindcss/typography@npm:0.5.10" +"@tailwindcss/typography@npm:^0.5.12": + version: 0.5.12 + resolution: "@tailwindcss/typography@npm:0.5.12" dependencies: lodash.castarray: "npm:^4.4.0" lodash.isplainobject: "npm:^4.0.6" @@ -713,7 +722,7 @@ __metadata: postcss-selector-parser: "npm:6.0.10" peerDependencies: tailwindcss: "*" - checksum: 10c0/0fa9c96bf091fb79fdc39a0244027b3891e9205f714197c8196e7ba2305523ce8695d14b912366de9b77d4b1d3a742fb7f9bc9bb633fddc7c7f13374b41075d2 + checksum: 10c0/3410b06594b89b5650cd4c985e6b4f48d1ba2cfbe146bb5ea2a2cfc1c9b065cd3ca2215436a814f458936683836f9cf792a8d6341345feccf6fb9910c565c092 languageName: node linkType: hard @@ -980,12 +989,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.11.30": - version: 20.11.30 - resolution: "@types/node@npm:20.11.30" +"@types/node@npm:^20.12.4": + version: 20.12.4 + resolution: "@types/node@npm:20.12.4" dependencies: undici-types: "npm:~5.26.4" - checksum: 10c0/867cfaf969c6d8850d8d7304e7ab739898a50ecb1395b61ff2335644f5f48d7a46fbc4a14cee967aed65ec134b61a746edae70d1f32f11321ccf29165e3bc4e6 + checksum: 10c0/9b142fcd839a48c348d6b9acfc753dfa4b3fb1f3e23ed67e8952bee9b2dfdaffdddfbcf0e4701557b88631591a5f9968433910027532ef847759f8682e27ffe7 languageName: node linkType: hard @@ -1003,12 +1012,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.22": - version: 18.2.22 - resolution: "@types/react-dom@npm:18.2.22" +"@types/react-dom@npm:^18.2.24": + version: 18.2.24 + resolution: "@types/react-dom@npm:18.2.24" dependencies: "@types/react": "npm:*" - checksum: 10c0/cd85b5f402126e44b8c7b573e74737389816abcc931b2b14d8f946ba81cce8637ea490419488fcae842efb1e2f69853bc30522e43fd8359e1007d4d14b8d8146 + checksum: 10c0/9ec38e5ab4727c56ef17bd8e938ead88748ba19db314b8d9807714a5cae430f5b799514667b221b4f2dc8d9b4ca17dd1c3da8c41c083c2de9eddcc31bec6b8ff languageName: node linkType: hard @@ -1023,14 +1032,13 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18.2.69": - version: 18.2.69 - resolution: "@types/react@npm:18.2.69" +"@types/react@npm:^18.2.74": + version: 18.2.74 + resolution: "@types/react@npm:18.2.74" dependencies: "@types/prop-types": "npm:*" - "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: 10c0/239b947ed8576a271057a6f571d0967098074b101a2bece244e88093dc8fb2f020872c463b8e78b2049334ba99158f6eef5960e51f73a389f86eee39b835d846 + checksum: 10c0/347e38b4c5dc20d50ff71bf04b7caaef490e5ff695e74a0088a13fbb2a0c5d125a5ecfd142adfa30f0176da0e2734942c91ba61d95ce269c43b3265bd7379361 languageName: node linkType: hard @@ -1392,7 +1400,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:^2.5.1": +"classnames@npm:2.5.1": version: 2.5.1 resolution: "classnames@npm:2.5.1" checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69 @@ -1511,12 +1519,12 @@ __metadata: languageName: node linkType: hard -"css-declaration-sorter@npm:^7.1.1": - version: 7.1.1 - resolution: "css-declaration-sorter@npm:7.1.1" +"css-declaration-sorter@npm:^7.2.0": + version: 7.2.0 + resolution: "css-declaration-sorter@npm:7.2.0" peerDependencies: postcss: ^8.0.9 - checksum: 10c0/bea446e441bafde21c3c7b3f7639559311da12eea140db7ee3c61e4f41df455b7b098df107f99bc0cca32a5020841cc94bf8a2d5efb1b383e51f9de478c4816e + checksum: 10c0/d8516be94f8f2daa233ef021688b965c08161624cbf830a4d7ee1099429437c0ee124d35c91b1c659cfd891a68e8888aa941726dab12279bc114aaed60a94606 languageName: node linkType: hard @@ -1588,12 +1596,12 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-default@npm:^6.1.1": - version: 6.1.1 - resolution: "cssnano-preset-default@npm:6.1.1" +"cssnano-preset-default@npm:^6.1.2": + version: 6.1.2 + resolution: "cssnano-preset-default@npm:6.1.2" dependencies: browserslist: "npm:^4.23.0" - css-declaration-sorter: "npm:^7.1.1" + css-declaration-sorter: "npm:^7.2.0" cssnano-utils: "npm:^4.0.2" postcss-calc: "npm:^9.0.1" postcss-colormin: "npm:^6.1.0" @@ -1624,7 +1632,7 @@ __metadata: postcss-unique-selectors: "npm:^6.0.4" peerDependencies: postcss: ^8.4.31 - checksum: 10c0/dc5927c8538778f859b781dc1fb10f0082cd7b3afdba30e835e472dbf51724d4f18e5170587761150142284a371d6c9d2c3b51d29826c08d2b9dd86a929597ee + checksum: 10c0/af99021f936763850f5f35dc9e6a9dfb0da30856dea36e0420b011da2a447099471db2a5f3d1f5f52c0489da186caf9a439d8f048a80f82617077efb018333fa languageName: node linkType: hard @@ -1637,15 +1645,15 @@ __metadata: languageName: node linkType: hard -"cssnano@npm:^6.1.1": - version: 6.1.1 - resolution: "cssnano@npm:6.1.1" +"cssnano@npm:^6.1.2": + version: 6.1.2 + resolution: "cssnano@npm:6.1.2" dependencies: - cssnano-preset-default: "npm:^6.1.1" + cssnano-preset-default: "npm:^6.1.2" lilconfig: "npm:^3.1.1" peerDependencies: postcss: ^8.4.31 - checksum: 10c0/2db52c2f5e314f05efec8977de392886ef0e7e08568ac45446f2303218180e317cee64c6c0c6d2c1d70a7f339fcead75384e09e187b88ccacd7f9fd51919fdf1 + checksum: 10c0/4df0dc0389b34b38acb09b7cfb07267b0eda95349c6d5e9b7666acc7200bb33359650869a60168e9d878298b05f4ad2c7f070815c90551720a3f4e1037f79691 languageName: node linkType: hard @@ -1672,7 +1680,7 @@ __metadata: languageName: node linkType: hard -"debounce@npm:^2.0.0": +"debounce@npm:2.0.0": version: 2.0.0 resolution: "debounce@npm:2.0.0" checksum: 10c0/35a492f7f4f346f0539d486d7e68a68bd2eaa0c7fb159df8e59f89a20afd9b0ed024d0dc5a6f283b6963f5f032dacd66d805a325e9d8338511e77b505b76f26e @@ -2037,25 +2045,26 @@ __metadata: languageName: node linkType: hard -"flowbite-react@npm:^0.7.5": - version: 0.7.5 - resolution: "flowbite-react@npm:0.7.5" +"flowbite-react@npm:^0.7.8": + version: 0.7.8 + resolution: "flowbite-react@npm:0.7.8" dependencies: - "@floating-ui/react": "npm:^0.26.2" - classnames: "npm:^2.5.1" - debounce: "npm:^2.0.0" - flowbite: "npm:^2.0.0" - react-icons: "npm:^4.11.0" - tailwind-merge: "npm:^2.0.0" + "@floating-ui/core": "npm:1.6.0" + "@floating-ui/react": "npm:0.26.10" + classnames: "npm:2.5.1" + debounce: "npm:2.0.0" + flowbite: "npm:2.3.0" + react-icons: "npm:5.0.1" + tailwind-merge: "npm:2.2.2" peerDependencies: - react: ^18 - react-dom: ^18 + react: ">=18" + react-dom: ">=18" tailwindcss: ^3 - checksum: 10c0/e1cfc73ab5926c41a9a7bad17ddc45b14b49c274b169eb091a3df10e29a9245632bde419d261cf1bce4836015773a9cfe36633dab3ee8c6321e6584f71f5f408 + checksum: 10c0/fd9fcccd7c6bc26bf09c60199e0e43ceb838266e40d53ca45ce5a77f0fea7b8f0b954d911c2f620181ca92a7529f6d989ea709019a4a2bf59011a9994f990c0c languageName: node linkType: hard -"flowbite@npm:^2.0.0, flowbite@npm:^2.3.0": +"flowbite@npm:2.3.0, flowbite@npm:^2.3.0": version: 2.3.0 resolution: "flowbite@npm:2.3.0" dependencies: @@ -2527,7 +2536,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.19.1": +"jiti@npm:^1.21.0": version: 1.21.0 resolution: "jiti@npm:1.21.0" bin: @@ -3853,16 +3862,16 @@ __metadata: languageName: node linkType: hard -"postcss-nesting@npm:^12.1.0": - version: 12.1.0 - resolution: "postcss-nesting@npm:12.1.0" +"postcss-nesting@npm:^12.1.1": + version: 12.1.1 + resolution: "postcss-nesting@npm:12.1.1" dependencies: "@csstools/selector-resolve-nested": "npm:^1.1.0" - "@csstools/selector-specificity": "npm:^3.0.2" + "@csstools/selector-specificity": "npm:^3.0.3" postcss-selector-parser: "npm:^6.0.13" peerDependencies: postcss: ^8.4 - checksum: 10c0/87902723214ec33a81520f51cce1e52f52c02272f2b7838d3aaa795e226104bf4809ef9dad8840f43852c1f229afb5f480ab1f20c7677bd021cf57739b625871 + checksum: 10c0/8fac718e69ee2ac93179cc59810a8184581c04715fe34621ec5d504fc680cad4a11219ed0c918cbe15c468994c9aba88e729f35eef698c5d44cadd824425c47d languageName: node linkType: hard @@ -4070,7 +4079,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.36, postcss@npm:^8.4.38": +"postcss@npm:^8.4.38": version: 8.4.38 resolution: "postcss@npm:8.4.38" dependencies: @@ -4164,16 +4173,7 @@ __metadata: languageName: node linkType: hard -"react-icons@npm:^4.11.0": - version: 4.12.0 - resolution: "react-icons@npm:4.12.0" - peerDependencies: - react: "*" - checksum: 10c0/2170f43031ee7365539f72d4075cbe6c7fbf9a66d6cf4494aa9393b194272da0564f5b19d1b24dbfc567c0ac89f5fe5b8974d92dd83f61e252388dde6a226fb8 - languageName: node - linkType: hard - -"react-icons@npm:^5.0.1": +"react-icons@npm:5.0.1, react-icons@npm:^5.0.1": version: 5.0.1 resolution: "react-icons@npm:5.0.1" peerDependencies: @@ -4573,27 +4573,27 @@ __metadata: "@tailwindcss/aspect-ratio": "npm:^0.4.2" "@tailwindcss/forms": "npm:^0.5.7" "@tailwindcss/line-clamp": "npm:^0.4.4" - "@tailwindcss/typography": "npm:^0.5.10" - "@types/node": "npm:^20.11.30" - "@types/react": "npm:^18.2.69" - "@types/react-dom": "npm:^18.2.22" + "@tailwindcss/typography": "npm:^0.5.12" + "@types/node": "npm:^20.12.4" + "@types/react": "npm:^18.2.74" + "@types/react-dom": "npm:^18.2.24" "@vitejs/plugin-react-swc": "npm:^3.6.0" autoprefixer: "npm:^10.4.19" axios: "npm:^1.6.8" bytemd: "npm:^1.21.0" cron-parser: "npm:^4.9.0" - cssnano: "npm:^6.1.1" + cssnano: "npm:^6.1.2" csstype: "npm:^3.1.3" dayjs: "npm:^1.11.10" flowbite: "npm:^2.3.0" - flowbite-react: "npm:^0.7.5" + flowbite-react: "npm:^0.7.8" human-format: "npm:^1.2.0" json-server: "npm:^1.0.0-alpha.23" lodash: "npm:^4.17.21" monaco-editor: "npm:^0.47.0" postcss: "npm:^8.4.38" postcss-import: "npm:^16.1.0" - postcss-nesting: "npm:^12.1.0" + postcss-nesting: "npm:^12.1.1" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" react-helmet-async: "npm:^2.0.4" @@ -4602,9 +4602,9 @@ __metadata: react-router-dom: "npm:^6.22.3" react-toastify: "npm:^10.0.5" react-use: "npm:^17.5.0" - tailwindcss: "npm:^3.4.1" - typescript: "npm:^5.4.3" - vite: "npm:^5.2.4" + tailwindcss: "npm:^3.4.3" + typescript: "npm:^5.4.4" + vite: "npm:^5.2.8" xterm: "npm:^5.3.0" xterm-addon-attach: "npm:^0.9.0" xterm-addon-fit: "npm:^0.8.0" @@ -4879,25 +4879,25 @@ __metadata: languageName: node linkType: hard -"tabbable@npm:^6.0.1": +"tabbable@npm:^6.0.0": version: 6.2.0 resolution: "tabbable@npm:6.2.0" checksum: 10c0/ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898 languageName: node linkType: hard -"tailwind-merge@npm:^2.0.0": - version: 2.2.1 - resolution: "tailwind-merge@npm:2.2.1" +"tailwind-merge@npm:2.2.2": + version: 2.2.2 + resolution: "tailwind-merge@npm:2.2.2" dependencies: - "@babel/runtime": "npm:^7.23.7" - checksum: 10c0/14ab965ec897e9377484b7593f7a700dde09b8035b762ad42652622a3ed1f202b203f48c0f235c0b1b38e9390470d94458f6f9010d33a5a18d71b15f38b986a6 + "@babel/runtime": "npm:^7.24.0" + checksum: 10c0/68a5e199848a467aed4f8d1a8d7b6a5b583ff72f1d2801e018bf245eaa41e6564b63ead9e2b708a214cefbd843970c5e0a21754d5f2a20e2c1238e25955685ce languageName: node linkType: hard -"tailwindcss@npm:^3.4.1": - version: 3.4.1 - resolution: "tailwindcss@npm:3.4.1" +"tailwindcss@npm:^3.4.3": + version: 3.4.3 + resolution: "tailwindcss@npm:3.4.3" dependencies: "@alloc/quick-lru": "npm:^5.2.0" arg: "npm:^5.0.2" @@ -4907,7 +4907,7 @@ __metadata: fast-glob: "npm:^3.3.0" glob-parent: "npm:^6.0.2" is-glob: "npm:^4.0.3" - jiti: "npm:^1.19.1" + jiti: "npm:^1.21.0" lilconfig: "npm:^2.1.0" micromatch: "npm:^4.0.5" normalize-path: "npm:^3.0.0" @@ -4924,7 +4924,7 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 10c0/eec3d758f1cd4f51ab3b4c201927c3ecd18e55f8ac94256af60276aaf8d1df78f9dddb5e9fb1e057dfa7cea3c1356add4994cc3d42da9739df874e67047e656f + checksum: 10c0/11e5546494f2888f693ebaa271b218b3a8e52fe59d7b629e54f2dffd6eaafd5ded2e9f0c37ad04e6a866dffb2b116d91becebad77e1441beee8bf016bb2392f9 languageName: node linkType: hard @@ -5041,23 +5041,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.4.3": - version: 5.4.3 - resolution: "typescript@npm:5.4.3" +"typescript@npm:^5.4.4": + version: 5.4.4 + resolution: "typescript@npm:5.4.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/22443a8760c3668e256c0b34b6b45c359ef6cecc10c42558806177a7d500ab1a7d7aac1f976d712e26989ddf6731d2fbdd3212b7c73290a45127c1c43ba2005a + checksum: 10c0/4d8de0291204ed61ca97ad0cba2ce064e09c4988ca1c451c787e4653ba76296ba35177a52694e8a00cf4ef899d0ee83338663b926d8b7d55167ff0ba81549999 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": - version: 5.4.3 - resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.4.4#optional!builtin": + version: 5.4.4 + resolution: "typescript@patch:typescript@npm%3A5.4.4#optional!builtin::version=5.4.4&hash=5adc0c" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/6e51f8b7e6ec55b897b9e56b67e864fe8f44e30f4a14357aad5dc0f7432db2f01efc0522df0b6c36d361c51f2dc3dcac5c832efd96a404cfabf884e915d38828 + checksum: 10c0/1fa41b9964a9ff0ed913b339c90b46031b2d2da3cb1a192af516610733f7f1d5f7f9754a8e22b9ac7076d3d8aedd2c4f84db3f113bad060eac3a95962443a1bf languageName: node linkType: hard @@ -5223,13 +5223,13 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.2.4": - version: 5.2.4 - resolution: "vite@npm:5.2.4" +"vite@npm:^5.2.8": + version: 5.2.8 + resolution: "vite@npm:5.2.8" dependencies: esbuild: "npm:^0.20.1" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.36" + postcss: "npm:^8.4.38" rollup: "npm:^4.13.0" peerDependencies: "@types/node": ^18.0.0 || >=20.0.0 @@ -5259,7 +5259,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/a8a57da83b5a46d9fc135fc4c51d08e1e60cdc4263ce6e7b23d60501c70605dd541726c331ab4d837ad774cdbf0b78bac088da770287620f81ad8b4d7b39dd74 + checksum: 10c0/b5717bb00c2570c08ff6d8ed917655e79184efcafa9dd62d52eea19c5d6dfc5a708ec3de9ebc670a7165fc5d401c2bdf1563bb39e2748d8e51e1593d286a9a13 languageName: node linkType: hard