diff --git a/internal/api/resolver.go b/internal/api/resolver.go index 5db47c3b9..bfe96939f 100644 --- a/internal/api/resolver.go +++ b/internal/api/resolver.go @@ -95,8 +95,12 @@ func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) err return txn.WithTxn(ctx, r.txnManager, fn) } +func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error { + return txn.WithReadTxn(ctx, r.txnManager, fn) +} + func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SceneMarker.Wall(ctx, q) return err }); err != nil { @@ -106,7 +110,7 @@ func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*model } func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Scene.Wall(ctx, q) return err }); err != nil { @@ -117,7 +121,7 @@ func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models } func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort) return err }); err != nil { @@ -129,7 +133,7 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) { var ret StatsResultType - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { repo := r.repository scenesQB := repo.Scene imageQB := repo.Image @@ -205,7 +209,7 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([ var keys []int tags := make(map[int]*SceneMarkerTag) - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID) if err != nil { return err diff --git a/internal/api/resolver_model_gallery.go b/internal/api/resolver_model_gallery.go index 1d1518b9b..72a061eb5 100644 --- a/internal/api/resolver_model_gallery.go +++ b/internal/api/resolver_model_gallery.go @@ -73,7 +73,7 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol var ret *file.Folder - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Folder.Find(ctx, *obj.FolderID) @@ -124,7 +124,7 @@ func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) } func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error // #2376 - sort images by path @@ -143,7 +143,7 @@ func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret } func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { // doing this via Query is really slow, so stick with FindByGalleryID imgs, err := r.repository.Image.FindByGalleryID(ctx, obj.ID) if err != nil { @@ -179,7 +179,7 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) { if !obj.Files.PrimaryLoaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadPrimaryFile(ctx, r.repository.File) }); err != nil { return "", err @@ -203,7 +203,7 @@ func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (* func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) { if !obj.SceneIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadSceneIDs(ctx, r.repository.Gallery) }); err != nil { return nil, err @@ -225,7 +225,7 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) { if !obj.TagIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadTagIDs(ctx, r.repository.Gallery) }); err != nil { return nil, err @@ -239,7 +239,7 @@ func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret [] func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) { if !obj.PerformerIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadPerformerIDs(ctx, r.repository.Gallery) }); err != nil { return nil, err @@ -252,7 +252,7 @@ func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) ( } func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID) return err diff --git a/internal/api/resolver_model_image.go b/internal/api/resolver_model_image.go index 4e6ef8605..c7fdb8c5f 100644 --- a/internal/api/resolver_model_image.go +++ b/internal/api/resolver_model_image.go @@ -132,7 +132,7 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePat func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) { if !obj.GalleryIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadGalleryIDs(ctx, r.repository.Image) }); err != nil { return nil, err @@ -166,7 +166,7 @@ func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *mod func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) { if !obj.TagIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadTagIDs(ctx, r.repository.Image) }); err != nil { return nil, err @@ -180,7 +180,7 @@ func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*mod func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) { if !obj.PerformerIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadPerformerIDs(ctx, r.repository.Image) }); err != nil { return nil, err diff --git a/internal/api/resolver_model_movie.go b/internal/api/resolver_model_movie.go index 5101dd4f9..fbde8a80a 100644 --- a/internal/api/resolver_model_movie.go +++ b/internal/api/resolver_model_movie.go @@ -94,7 +94,7 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) ( func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) { // don't return any thing if there is no back image var img []byte - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error img, err = r.repository.Movie.GetBackImage(ctx, obj.ID) if err != nil { @@ -117,7 +117,7 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (* func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID) return err }); err != nil { @@ -128,7 +128,7 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret } func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID) return err diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index 2bb297a3d..414b894a4 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -37,7 +37,7 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer } func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID) return err }); err != nil { @@ -49,7 +49,7 @@ func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (re func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID) return err }); err != nil { @@ -61,7 +61,7 @@ func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performe func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID) return err }); err != nil { @@ -73,7 +73,7 @@ func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performe func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID) return err }); err != nil { @@ -84,7 +84,7 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor } func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID) return err }); err != nil { @@ -96,7 +96,7 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) ( func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) { var ret []models.StashID - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID) return err @@ -128,7 +128,7 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer } func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID) return err }); err != nil { @@ -140,7 +140,7 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) ( func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID) return err }); err != nil { diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go index df58e8e86..47a4d0382 100644 --- a/internal/api/resolver_model_scene.go +++ b/internal/api/resolver_model_scene.go @@ -206,7 +206,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat } func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID) return err }); err != nil { @@ -225,7 +225,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret [] return nil, nil } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID) return err }); err != nil { @@ -237,7 +237,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret [] func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) { if !obj.GalleryIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadGalleryIDs(ctx, r.repository.Scene) }); err != nil { return nil, err @@ -259,7 +259,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) { if !obj.Movies.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene return obj.LoadMovies(ctx, qb) @@ -290,7 +290,7 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) { if !obj.TagIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadTagIDs(ctx, r.repository.Scene) }); err != nil { return nil, err @@ -304,7 +304,7 @@ func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*mod func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) { if !obj.PerformerIDs.Loaded() { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadPerformerIDs(ctx, r.repository.Scene) }); err != nil { return nil, err @@ -327,7 +327,7 @@ func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID { } func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadStashIDs(ctx, r.repository.Scene) }); err != nil { return nil, err diff --git a/internal/api/resolver_model_scene_marker.go b/internal/api/resolver_model_scene_marker.go index 7a4d01be1..0057db4e8 100644 --- a/internal/api/resolver_model_scene_marker.go +++ b/internal/api/resolver_model_scene_marker.go @@ -13,7 +13,7 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker panic("Invalid scene id") } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { sceneID := int(obj.SceneID.Int64) ret, err = r.repository.Scene.Find(ctx, sceneID) return err @@ -25,7 +25,7 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker } func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (ret *models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.Find(ctx, obj.PrimaryTagID) return err }); err != nil { @@ -36,7 +36,7 @@ func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneM } func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.FindBySceneMarkerID(ctx, obj.ID) return err }); err != nil { diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 5b8b15d21..282e5a46e 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -30,7 +30,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL() var hasImage bool - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID) return err @@ -47,7 +47,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st } func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Studio.GetAliases(ctx, obj.ID) return err }); err != nil { @@ -59,7 +59,7 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret [ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID) return err }); err != nil { @@ -71,7 +71,7 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (re func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID) return err }); err != nil { @@ -83,7 +83,7 @@ func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (re func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID) return err }); err != nil { @@ -102,7 +102,7 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) ( } func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Studio.FindChildren(ctx, obj.ID) return err }); err != nil { @@ -114,7 +114,7 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) ( func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) { var ret []models.StashID - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID) return err @@ -157,7 +157,7 @@ func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*ti } func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID) return err }); err != nil { @@ -169,7 +169,7 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret [] func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID) return err }); err != nil { diff --git a/internal/api/resolver_model_tag.go b/internal/api/resolver_model_tag.go index db6236a0b..70fee39e0 100644 --- a/internal/api/resolver_model_tag.go +++ b/internal/api/resolver_model_tag.go @@ -18,7 +18,7 @@ func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string } func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID) return err }); err != nil { @@ -29,7 +29,7 @@ func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*mode } func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID) return err }); err != nil { @@ -40,7 +40,7 @@ func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*mod } func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.GetAliases(ctx, obj.ID) return err }); err != nil { @@ -52,7 +52,7 @@ func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []strin func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var count int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { count, err = r.repository.Scene.CountByTagID(ctx, obj.ID) return err }); err != nil { @@ -64,7 +64,7 @@ func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var count int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { count, err = r.repository.SceneMarker.CountByTagID(ctx, obj.ID) return err }); err != nil { @@ -76,7 +76,7 @@ func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (re func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = image.CountByTagID(ctx, r.repository.Image, obj.ID) return err }); err != nil { @@ -88,7 +88,7 @@ func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var res int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { res, err = gallery.CountByTagID(ctx, r.repository.Gallery, obj.ID) return err }); err != nil { @@ -100,7 +100,7 @@ func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *i func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var count int - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { count, err = r.repository.Performer.CountByTagID(ctx, obj.ID) return err }); err != nil { diff --git a/internal/api/resolver_mutation_stash_box.go b/internal/api/resolver_mutation_stash_box.go index 22cc1799e..d1a7e2de2 100644 --- a/internal/api/resolver_mutation_stash_box.go +++ b/internal/api/resolver_mutation_stash_box.go @@ -51,7 +51,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S } var res *string - err = r.withTxn(ctx, func(ctx context.Context) error { + err = r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene scene, err := qb.Find(ctx, id) if err != nil { @@ -82,7 +82,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp } var res *string - err = r.withTxn(ctx, func(ctx context.Context) error { + err = r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Performer performer, err := qb.Find(ctx, id) if err != nil { diff --git a/internal/api/resolver_query_find_gallery.go b/internal/api/resolver_query_find_gallery.go index ee12471d1..e8d47d70b 100644 --- a/internal/api/resolver_query_find_gallery.go +++ b/internal/api/resolver_query_find_gallery.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Gallery.Find(ctx, idInt) return err }); err != nil { @@ -24,7 +24,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models } func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter) if err != nil { return err diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index ad9bf6c94..6468ba9f3 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -12,7 +12,7 @@ import ( func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) { var image *models.Image - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Image var err error @@ -47,7 +47,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str } func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Image fields := graphql.CollectAllFields(ctx) diff --git a/internal/api/resolver_query_find_movie.go b/internal/api/resolver_query_find_movie.go index 7505c7f36..a7e72dbdc 100644 --- a/internal/api/resolver_query_find_movie.go +++ b/internal/api/resolver_query_find_movie.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.Find(ctx, idInt) return err }); err != nil { @@ -24,7 +24,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M } func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter) if err != nil { return err @@ -44,7 +44,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi } func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.All(ctx) return err }); err != nil { diff --git a/internal/api/resolver_query_find_performer.go b/internal/api/resolver_query_find_performer.go index 4314b0f69..437ac8fcf 100644 --- a/internal/api/resolver_query_find_performer.go +++ b/internal/api/resolver_query_find_performer.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Performer.Find(ctx, idInt) return err }); err != nil { @@ -24,7 +24,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode } func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { performers, total, err := r.repository.Performer.Query(ctx, performerFilter, filter) if err != nil { return err @@ -43,7 +43,7 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod } func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Performer.All(ctx) return err }); err != nil { diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index 7b934f581..4f196fd65 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.Find(ctx, idInt) return err }); err != nil { @@ -23,7 +23,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo } func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { if mode != nil { ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode) } else { @@ -37,7 +37,7 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte } func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.FindDefault(ctx, mode) return err }); err != nil { diff --git a/internal/api/resolver_query_find_scene.go b/internal/api/resolver_query_find_scene.go index 9f049805f..95519cd49 100644 --- a/internal/api/resolver_query_find_scene.go +++ b/internal/api/resolver_query_find_scene.go @@ -12,7 +12,7 @@ import ( func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) { var scene *models.Scene - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene var err error if id != nil { @@ -43,7 +43,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) { var scene *models.Scene - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene if input.Checksum != nil { scenes, err := qb.FindByChecksum(ctx, *input.Checksum) @@ -74,7 +74,7 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu } func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var scenes []*models.Scene var err error @@ -135,7 +135,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen } func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { sceneFilter := &models.SceneFilterType{} @@ -192,7 +192,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config manager.SceneParserInput) (ret *SceneParserResultType, err error) { parser := manager.NewSceneFilenameParser(filter, config) - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { result, count, err := parser.Parse(ctx, manager.SceneFilenameParserRepository{ Scene: r.repository.Scene, Performer: r.repository.Performer, @@ -223,7 +223,7 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int) if distance != nil { dist = *distance } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Scene.FindDuplicates(ctx, dist) return err }); err != nil { diff --git a/internal/api/resolver_query_find_scene_marker.go b/internal/api/resolver_query_find_scene_marker.go index 03b9e261a..4bd70e658 100644 --- a/internal/api/resolver_query_find_scene_marker.go +++ b/internal/api/resolver_query_find_scene_marker.go @@ -7,7 +7,7 @@ import ( ) func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter) if err != nil { return err diff --git a/internal/api/resolver_query_find_studio.go b/internal/api/resolver_query_find_studio.go index 0bd17b9ad..51cac6208 100644 --- a/internal/api/resolver_query_find_studio.go +++ b/internal/api/resolver_query_find_studio.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models. return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { var err error ret, err = r.repository.Studio.Find(ctx, idInt) return err @@ -25,7 +25,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models. } func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter) if err != nil { return err @@ -45,7 +45,7 @@ func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.St } func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Studio.All(ctx) return err }); err != nil { diff --git a/internal/api/resolver_query_find_tag.go b/internal/api/resolver_query_find_tag.go index 77bd57f98..fd4b04ad2 100644 --- a/internal/api/resolver_query_find_tag.go +++ b/internal/api/resolver_query_find_tag.go @@ -13,7 +13,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag return nil, err } - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.Find(ctx, idInt) return err }); err != nil { @@ -24,7 +24,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag } func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter) if err != nil { return err @@ -44,7 +44,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte } func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) { - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.All(ctx) return err }); err != nil { diff --git a/internal/api/resolver_query_scene.go b/internal/api/resolver_query_scene.go index b6da7b901..f4dea464e 100644 --- a/internal/api/resolver_query_scene.go +++ b/internal/api/resolver_query_scene.go @@ -14,7 +14,7 @@ import ( func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manager.SceneStreamEndpoint, error) { // find the scene var scene *models.Scene - if err := r.withTxn(ctx, func(ctx context.Context) error { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { idInt, _ := strconv.Atoi(*id) var err error scene, err = r.repository.Scene.Find(ctx, idInt) diff --git a/internal/api/routes_image.go b/internal/api/routes_image.go index b89821155..7ac8c99ae 100644 --- a/internal/api/routes_image.go +++ b/internal/api/routes_image.go @@ -143,7 +143,7 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler { imageID, _ := strconv.Atoi(imageIdentifierQueryParam) var image *models.Image - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { qb := rs.imageFinder if imageID == 0 { images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam) diff --git a/internal/api/routes_movie.go b/internal/api/routes_movie.go index 032fefca1..c29718566 100644 --- a/internal/api/routes_movie.go +++ b/internal/api/routes_movie.go @@ -41,7 +41,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) { defaultParam := r.URL.Query().Get("default") var image []byte if defaultParam != "true" { - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.movieFinder.GetFrontImage(ctx, movie.ID) return nil }) @@ -67,7 +67,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) { defaultParam := r.URL.Query().Get("default") var image []byte if defaultParam != "true" { - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.movieFinder.GetBackImage(ctx, movie.ID) return nil }) @@ -97,7 +97,7 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler { } var movie *models.Movie - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { movie, _ = rs.movieFinder.Find(ctx, movieID) return nil }) diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go index dcc338103..c8295467a 100644 --- a/internal/api/routes_performer.go +++ b/internal/api/routes_performer.go @@ -41,7 +41,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.performerFinder.GetImage(ctx, performer.ID) return nil }) @@ -71,7 +71,7 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler { } var performer *models.Performer - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error performer, err = rs.performerFinder.Find(ctx, performerID) return err diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index 27693a517..d1b1b02c8 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -264,7 +264,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce } var title string - if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error { qb := rs.tagFinder primaryTag, err := qb.Find(ctx, marker.PrimaryTagID) if err != nil { @@ -293,7 +293,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) var sceneMarkers []*models.SceneMarker - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID) return err @@ -349,7 +349,7 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin s := r.Context().Value(sceneKey).(*models.Scene) var captions []*models.VideoCaption - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error primaryFile := s.Files.Primary() if primaryFile == nil { @@ -423,7 +423,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err @@ -450,7 +450,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err @@ -487,7 +487,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err @@ -528,7 +528,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler { sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam) var scene *models.Scene - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { qb := rs.sceneFinder if sceneID == 0 { var scenes []*models.Scene diff --git a/internal/api/routes_studio.go b/internal/api/routes_studio.go index c0b51b715..2ddeb51a3 100644 --- a/internal/api/routes_studio.go +++ b/internal/api/routes_studio.go @@ -41,7 +41,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.studioFinder.GetImage(ctx, studio.ID) return nil }) @@ -71,7 +71,7 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler { } var studio *models.Studio - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error studio, err = rs.studioFinder.Find(ctx, studioID) return err diff --git a/internal/api/routes_tag.go b/internal/api/routes_tag.go index 1773e0daa..1f72928c2 100644 --- a/internal/api/routes_tag.go +++ b/internal/api/routes_tag.go @@ -41,7 +41,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.tagFinder.GetImage(ctx, tag.ID) return nil }) @@ -71,7 +71,7 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler { } var tag *models.Tag - _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error tag, err = rs.tagFinder.Find(ctx, tagID) return err diff --git a/internal/dlna/cds.go b/internal/dlna/cds.go index 948a808d6..4deb017f2 100644 --- a/internal/dlna/cds.go +++ b/internal/dlna/cds.go @@ -360,7 +360,7 @@ func (me *contentDirectoryService) handleBrowseMetadata(obj object, host string) } else { var scene *models.Scene - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { scene, err = me.repository.SceneFinder.Find(ctx, sceneID) if scene != nil { err = scene.LoadPrimaryFile(ctx, me.repository.FileFinder) @@ -443,7 +443,7 @@ func getRootObjects() []interface{} { func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType, parentID string, host string) []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { sort := "title" findFilter := &models.FindFilterType{ PerPage: &pageSize, @@ -486,7 +486,7 @@ func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilterType, parentID string, page int, host string) []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { pager := scenePager{ sceneFilter: sceneFilter, parentID: parentID, @@ -527,7 +527,7 @@ func (me *contentDirectoryService) getAllScenes(host string) []interface{} { func (me *contentDirectoryService) getStudios() []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { studios, err := me.repository.StudioFinder.All(ctx) if err != nil { return err @@ -566,7 +566,7 @@ func (me *contentDirectoryService) getStudioScenes(paths []string, host string) func (me *contentDirectoryService) getTags() []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { tags, err := me.repository.TagFinder.All(ctx) if err != nil { return err @@ -605,7 +605,7 @@ func (me *contentDirectoryService) getTagScenes(paths []string, host string) []i func (me *contentDirectoryService) getPerformers() []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { performers, err := me.repository.PerformerFinder.All(ctx) if err != nil { return err @@ -644,7 +644,7 @@ func (me *contentDirectoryService) getPerformerScenes(paths []string, host strin func (me *contentDirectoryService) getMovies() []interface{} { var objs []interface{} - if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error { movies, err := me.repository.MovieFinder.All(ctx) if err != nil { return err diff --git a/internal/dlna/dms.go b/internal/dlna/dms.go index d5e7cc84e..fdef80db1 100644 --- a/internal/dlna/dms.go +++ b/internal/dlna/dms.go @@ -439,7 +439,7 @@ func (me *Server) serveIcon(w http.ResponseWriter, r *http.Request) { } var scene *models.Scene - err := txn.WithTxn(r.Context(), me.txnManager, func(ctx context.Context) error { + err := txn.WithReadTxn(r.Context(), me.txnManager, func(ctx context.Context) error { idInt, err := strconv.Atoi(sceneId) if err != nil { return nil @@ -579,7 +579,7 @@ func (me *Server) initMux(mux *http.ServeMux) { mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) { sceneId := r.URL.Query().Get("scene") var scene *models.Scene - err := txn.WithTxn(r.Context(), me.txnManager, func(ctx context.Context) error { + err := txn.WithReadTxn(r.Context(), me.txnManager, func(ctx context.Context) error { sceneIdInt, err := strconv.Atoi(sceneId) if err != nil { return nil diff --git a/internal/manager/repository.go b/internal/manager/repository.go index ac8ddbb8c..713e017b4 100644 --- a/internal/manager/repository.go +++ b/internal/manager/repository.go @@ -67,6 +67,10 @@ func (r *Repository) WithTxn(ctx context.Context, fn txn.TxnFunc) error { return txn.WithTxn(ctx, r, fn) } +func (r *Repository) WithReadTxn(ctx context.Context, fn txn.TxnFunc) error { + return txn.WithReadTxn(ctx, r, fn) +} + func (r *Repository) WithDB(ctx context.Context, fn txn.TxnFunc) error { return txn.WithDatabase(ctx, r, fn) } diff --git a/internal/manager/running_streams.go b/internal/manager/running_streams.go index 38286121e..23b00b59e 100644 --- a/internal/manager/running_streams.go +++ b/internal/manager/running_streams.go @@ -103,7 +103,7 @@ func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter } var cover []byte - readTxnErr := txn.WithTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { + readTxnErr := txn.WithReadTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID) return nil }) diff --git a/internal/manager/task_autotag.go b/internal/manager/task_autotag.go index 8ea8888ff..16df5d240 100644 --- a/internal/manager/task_autotag.go +++ b/internal/manager/task_autotag.go @@ -73,7 +73,7 @@ func (j *autoTagJob) autoTagSpecific(ctx context.Context, progress *job.Progress studioCount := len(studioIds) tagCount := len(tagIds) - if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := j.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { r := j.txnManager performerQuery := r.Performer studioQuery := r.Studio @@ -497,7 +497,7 @@ func (t *autoTagFilesTask) processScenes(ctx context.Context, r Repository) erro more := true for more { var scenes []*models.Scene - if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := t.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { var err error scenes, err = scene.Query(ctx, r.Scene, sceneFilter, findFilter) return err @@ -554,7 +554,7 @@ func (t *autoTagFilesTask) processImages(ctx context.Context, r Repository) erro more := true for more { var images []*models.Image - if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := t.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { var err error images, err = image.Query(ctx, r.Image, imageFilter, findFilter) return err @@ -611,7 +611,7 @@ func (t *autoTagFilesTask) processGalleries(ctx context.Context, r Repository) e more := true for more { var galleries []*models.Gallery - if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := t.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { var err error galleries, _, err = r.Gallery.Query(ctx, galleryFilter, findFilter) return err @@ -657,7 +657,7 @@ func (t *autoTagFilesTask) processGalleries(ctx context.Context, r Repository) e func (t *autoTagFilesTask) process(ctx context.Context) { r := t.txnManager - if err := r.WithTxn(ctx, func(ctx context.Context) error { + if err := r.WithReadTxn(ctx, func(ctx context.Context) error { total, err := t.getCount(ctx, t.txnManager) if err != nil { return err diff --git a/internal/manager/task_generate.go b/internal/manager/task_generate.go index de4d6c3b0..088a9ea3c 100644 --- a/internal/manager/task_generate.go +++ b/internal/manager/task_generate.go @@ -109,7 +109,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { Overwrite: j.overwrite, } - if err := j.txnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := j.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { qb := j.txnManager.Scene if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 { totals = j.queueTasks(ctx, g, queue) @@ -137,7 +137,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { } return nil - }); err != nil { + }); err != nil && ctx.Err() == nil { logger.Error(err.Error()) return } diff --git a/internal/manager/task_generate_interactive_heatmap_speed.go b/internal/manager/task_generate_interactive_heatmap_speed.go index 6aa2da049..8bb80354c 100644 --- a/internal/manager/task_generate_interactive_heatmap_speed.go +++ b/internal/manager/task_generate_interactive_heatmap_speed.go @@ -46,10 +46,9 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) { primaryFile.InteractiveSpeed = &median qb := t.TxnManager.File return qb.Update(ctx, primaryFile) - }); err != nil { + }); err != nil && ctx.Err() == nil { logger.Error(err.Error()) } - } func (t *GenerateInteractiveHeatmapSpeedTask) shouldGenerate() bool { diff --git a/internal/manager/task_generate_markers.go b/internal/manager/task_generate_markers.go index 0ef01c9d6..32bd2d5ef 100644 --- a/internal/manager/task_generate_markers.go +++ b/internal/manager/task_generate_markers.go @@ -42,7 +42,7 @@ func (t *GenerateMarkersTask) Start(ctx context.Context) { if t.Marker != nil { var scene *models.Scene - if err := t.TxnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := t.TxnManager.WithReadTxn(ctx, func(ctx context.Context) error { var err error scene, err = t.TxnManager.Scene.Find(ctx, int(t.Marker.SceneID.Int64)) if err == nil && scene != nil { @@ -73,7 +73,7 @@ func (t *GenerateMarkersTask) Start(ctx context.Context) { func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) { var sceneMarkers []*models.SceneMarker - if err := t.TxnManager.WithTxn(ctx, func(ctx context.Context) error { + if err := t.TxnManager.WithReadTxn(ctx, func(ctx context.Context) error { var err error sceneMarkers, err = t.TxnManager.SceneMarker.FindBySceneID(ctx, t.Scene.ID) return err diff --git a/internal/manager/task_generate_phash.go b/internal/manager/task_generate_phash.go index a986c96f1..6ba840694 100644 --- a/internal/manager/task_generate_phash.go +++ b/internal/manager/task_generate_phash.go @@ -44,8 +44,8 @@ func (t *GeneratePhashTask) Start(ctx context.Context) { }) return qb.Update(ctx, t.File) - }); err != nil { - logger.Error(err.Error()) + }); err != nil && ctx.Err() == nil { + logger.Errorf("Error setting phash: %v", err) } } diff --git a/internal/manager/task_generate_screenshot.go b/internal/manager/task_generate_screenshot.go index 2b5795777..c235d00b1 100644 --- a/internal/manager/task_generate_screenshot.go +++ b/internal/manager/task_generate_screenshot.go @@ -91,7 +91,7 @@ func (t *GenerateScreenshotTask) Start(ctx context.Context) { } return nil - }); err != nil { + }); err != nil && ctx.Err() == nil { logger.Error(err.Error()) } } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 33b26d689..a9f7fd4ad 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -50,7 +50,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) { if t.refresh { var performerID string - txnErr := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { + txnErr := txn.WithReadTxn(ctx, instance.Repository, func(ctx context.Context) error { stashids, _ := instance.Repository.Performer.GetStashIDs(ctx, t.performer.ID) for _, id := range stashids { if id.Endpoint == t.box.Endpoint { diff --git a/pkg/file/clean.go b/pkg/file/clean.go index 7a1ff912a..cc8ebde6b 100644 --- a/pkg/file/clean.go +++ b/pkg/file/clean.go @@ -109,7 +109,7 @@ func (j *cleanJob) execute(ctx context.Context) error { folderCount int ) - if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, j.Repository, func(ctx context.Context) error { var err error fileCount, err = j.Repository.CountAllInPaths(ctx, j.options.Paths) if err != nil { @@ -169,7 +169,7 @@ func (j *cleanJob) assessFiles(ctx context.Context, toDelete *deleteSet) error { progress := j.progress more := true - if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, j.Repository, func(ctx context.Context) error { for more { if job.IsCancelled(ctx) { return nil @@ -253,7 +253,7 @@ func (j *cleanJob) assessFolders(ctx context.Context, toDelete *deleteSet) error progress := j.progress more := true - if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, j.Repository, func(ctx context.Context) error { for more { if job.IsCancelled(ctx) { return nil diff --git a/pkg/file/scan.go b/pkg/file/scan.go index 1d7830f7e..31cd50af6 100644 --- a/pkg/file/scan.go +++ b/pkg/file/scan.go @@ -85,7 +85,6 @@ type scanJob struct { startTime time.Time fileQueue chan scanFile - dbQueue chan func(ctx context.Context) error retryList []scanFile retrying bool folderPathToID sync.Map @@ -148,9 +147,11 @@ func (s *scanJob) execute(ctx context.Context) { s.startTime = time.Now() s.fileQueue = make(chan scanFile, scanQueueSize) - s.dbQueue = make(chan func(ctx context.Context) error, scanQueueSize) + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() if err := s.queueFiles(ctx, paths); err != nil { if errors.Is(err, context.Canceled) { return @@ -163,6 +164,8 @@ func (s *scanJob) execute(ctx context.Context) { logger.Infof("Finished adding files to queue. %d files queued", s.count) }() + defer wg.Wait() + if err := s.processQueue(ctx); err != nil { if errors.Is(err, context.Canceled) { return @@ -329,38 +332,50 @@ func (s *scanJob) processQueue(ctx context.Context) error { wg := sizedwaitgroup.New(parallelTasks) - for f := range s.fileQueue { - if err := ctx.Err(); err != nil { - return err + if err := func() error { + defer wg.Wait() + + for f := range s.fileQueue { + if err := ctx.Err(); err != nil { + return err + } + + wg.Add() + ff := f + go func() { + defer wg.Done() + s.processQueueItem(ctx, ff) + }() } - wg.Add() - ff := f - go func() { - defer wg.Done() - s.processQueueItem(ctx, ff) - }() + return nil + }(); err != nil { + return err } - wg.Wait() s.retrying = true - for _, f := range s.retryList { - if err := ctx.Err(); err != nil { - return err + + if err := func() error { + defer wg.Wait() + + for _, f := range s.retryList { + if err := ctx.Err(); err != nil { + return err + } + + wg.Add() + ff := f + go func() { + defer wg.Done() + s.processQueueItem(ctx, ff) + }() } - wg.Add() - ff := f - go func() { - defer wg.Done() - s.processQueueItem(ctx, ff) - }() + return nil + }(); err != nil { + return err } - wg.Wait() - - close(s.dbQueue) - return nil } diff --git a/pkg/image/scan.go b/pkg/image/scan.go index 628a33d50..c8d02c26f 100644 --- a/pkg/image/scan.go +++ b/pkg/image/scan.go @@ -73,26 +73,6 @@ func (h *ScanHandler) validate() error { return nil } -func (h *ScanHandler) logInfo(ctx context.Context, format string, args ...interface{}) { - // log at the end so that if anything fails above due to a locked database - // error and the transaction must be retried, then we shouldn't get multiple - // logs of the same thing. - txn.AddPostCompleteHook(ctx, func(ctx context.Context) error { - logger.Infof(format, args...) - return nil - }) -} - -func (h *ScanHandler) logError(ctx context.Context, format string, args ...interface{}) { - // log at the end so that if anything fails above due to a locked database - // error and the transaction must be retried, then we shouldn't get multiple - // logs of the same thing. - txn.AddPostCompleteHook(ctx, func(ctx context.Context) error { - logger.Errorf(format, args...) - return nil - }) -} - func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File) error { if err := h.validate(); err != nil { return err @@ -132,7 +112,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File GalleryIDs: models.NewRelatedIDs([]int{}), } - h.logInfo(ctx, "%s doesn't exist. Creating new image...", f.Base().Path) + logger.Infof("%s doesn't exist. Creating new image...", f.Base().Path) if _, err := h.associateGallery(ctx, newImage, imageFile); err != nil { return err @@ -162,12 +142,17 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File } if h.ScanConfig.IsGenerateThumbnails() { - for _, s := range existing { - if err := h.ThumbnailGenerator.GenerateThumbnail(ctx, s, imageFile); err != nil { - // just log if cover generation fails. We can try again on rescan - h.logError(ctx, "Error generating thumbnail for %s: %v", imageFile.Path, err) + // do this after the commit so that the transaction isn't held up + txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + for _, s := range existing { + if err := h.ThumbnailGenerator.GenerateThumbnail(ctx, s, imageFile); err != nil { + // just log if cover generation fails. We can try again on rescan + logger.Errorf("Error generating thumbnail for %s: %v", imageFile.Path, err) + } } - } + + return nil + }) } return nil @@ -202,7 +187,7 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models. } if !found { - h.logInfo(ctx, "Adding %s to image %s", f.Path, i.DisplayName()) + logger.Infof("Adding %s to image %s", f.Path, i.DisplayName()) if err := h.CreatorUpdater.AddFileID(ctx, i.ID, f.ID); err != nil { return fmt.Errorf("adding file to image: %w", err) @@ -249,7 +234,7 @@ func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f file. UpdatedAt: now, } - h.logInfo(ctx, "Creating folder-based gallery for %s", filepath.Dir(f.Base().Path)) + logger.Infof("Creating folder-based gallery for %s", filepath.Dir(f.Base().Path)) if err := h.GalleryFinder.Create(ctx, newGallery, nil); err != nil { return nil, fmt.Errorf("creating folder based gallery: %w", err) @@ -273,7 +258,7 @@ func (h *ScanHandler) associateFolderImages(ctx context.Context, g *models.Galle } for _, ii := range i { - h.logInfo(ctx, "Adding %s to gallery %s", ii.Path, g.Path) + logger.Infof("Adding %s to gallery %s", ii.Path, g.Path) if _, err := h.CreatorUpdater.UpdatePartial(ctx, ii.ID, models.ImagePartial{ GalleryIDs: &models.UpdateIDs{ @@ -307,7 +292,7 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile fi UpdatedAt: now, } - h.logInfo(ctx, "%s doesn't exist. Creating new gallery...", zipFile.Base().Path) + logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path) if err := h.GalleryFinder.Create(ctx, newGallery, []file.ID{zipFile.Base().ID}); err != nil { return nil, fmt.Errorf("creating zip-based gallery: %w", err) @@ -345,7 +330,7 @@ func (h *ScanHandler) associateGallery(ctx context.Context, newImage *models.Ima if g != nil && !intslice.IntInclude(newImage.GalleryIDs.List(), g.ID) { ret = true newImage.GalleryIDs.Add(g.ID) - h.logInfo(ctx, "Adding %s to gallery %s", f.Base().Path, g.Path) + logger.Infof("Adding %s to gallery %s", f.Base().Path, g.Path) } return ret, nil diff --git a/pkg/models/mocks/transaction.go b/pkg/models/mocks/transaction.go index 0690ae419..f2c4c9c49 100644 --- a/pkg/models/mocks/transaction.go +++ b/pkg/models/mocks/transaction.go @@ -9,7 +9,7 @@ import ( type TxnManager struct{} -func (*TxnManager) Begin(ctx context.Context) (context.Context, error) { +func (*TxnManager) Begin(ctx context.Context, exclusive bool) (context.Context, error) { return ctx, nil } @@ -25,6 +25,9 @@ func (*TxnManager) Rollback(ctx context.Context) error { return nil } +func (*TxnManager) Complete(ctx context.Context) { +} + func (*TxnManager) AddPostCommitHook(ctx context.Context, hook txn.TxnFunc) { } diff --git a/pkg/models/repository.go b/pkg/models/repository.go index 45d6c0357..7a9e14af5 100644 --- a/pkg/models/repository.go +++ b/pkg/models/repository.go @@ -1,8 +1,6 @@ package models import ( - "context" - "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/txn" ) @@ -29,7 +27,3 @@ type Repository struct { Tag TagReaderWriter SavedFilter SavedFilterReaderWriter } - -func (r *Repository) WithTxn(ctx context.Context, fn txn.TxnFunc) error { - return txn.WithTxn(ctx, r, fn) -} diff --git a/pkg/scene/scan.go b/pkg/scene/scan.go index 48cd7dfc9..b0f9ef3d4 100644 --- a/pkg/scene/scan.go +++ b/pkg/scene/scan.go @@ -11,6 +11,7 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/paths" "github.com/stashapp/stash/pkg/plugin" + "github.com/stashapp/stash/pkg/txn" ) var ( @@ -119,17 +120,22 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File } } - for _, s := range existing { - if err := h.CoverGenerator.GenerateCover(ctx, s, videoFile); err != nil { - // just log if cover generation fails. We can try again on rescan - logger.Errorf("Error generating cover for %s: %v", videoFile.Path, err) + // do this after the commit so that cover generation doesn't hold up the transaction + txn.AddPostCommitHook(ctx, func(ctx context.Context) error { + for _, s := range existing { + if err := h.CoverGenerator.GenerateCover(ctx, s, videoFile); err != nil { + // just log if cover generation fails. We can try again on rescan + logger.Errorf("Error generating cover for %s: %v", videoFile.Path, err) + } + + if err := h.ScanGenerator.Generate(ctx, s, videoFile); err != nil { + // just log if cover generation fails. We can try again on rescan + logger.Errorf("Error generating content for %s: %v", videoFile.Path, err) + } } - if err := h.ScanGenerator.Generate(ctx, s, videoFile); err != nil { - // just log if cover generation fails. We can try again on rescan - logger.Errorf("Error generating content for %s: %v", videoFile.Path, err) - } - } + return nil + }) return nil } diff --git a/pkg/scraper/autotag.go b/pkg/scraper/autotag.go index f81035131..53aedc749 100644 --- a/pkg/scraper/autotag.go +++ b/pkg/scraper/autotag.go @@ -95,7 +95,7 @@ func (s autotagScraper) viaScene(ctx context.Context, _client *http.Client, scen const trimExt = false // populate performers, studio and tags based on scene path - if err := txn.WithTxn(ctx, s.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, s.txnManager, func(ctx context.Context) error { path := scene.Path if path == "" { return nil @@ -144,7 +144,7 @@ func (s autotagScraper) viaGallery(ctx context.Context, _client *http.Client, ga var ret *ScrapedGallery // populate performers, studio and tags based on scene path - if err := txn.WithTxn(ctx, s.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, s.txnManager, func(ctx context.Context) error { path := gallery.Path performers, err := autotagMatchPerformers(ctx, path, s.performerReader, trimExt) if err != nil { diff --git a/pkg/scraper/cache.go b/pkg/scraper/cache.go index 64cd63629..894286c3c 100644 --- a/pkg/scraper/cache.go +++ b/pkg/scraper/cache.go @@ -350,7 +350,7 @@ func (c Cache) ScrapeID(ctx context.Context, scraperID string, id int, ty Scrape func (c Cache) getScene(ctx context.Context, sceneID int) (*models.Scene, error) { var ret *models.Scene - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { var err error ret, err = c.repository.SceneFinder.Find(ctx, sceneID) return err @@ -362,7 +362,7 @@ func (c Cache) getScene(ctx context.Context, sceneID int) (*models.Scene, error) func (c Cache) getGallery(ctx context.Context, galleryID int) (*models.Gallery, error) { var ret *models.Gallery - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { var err error ret, err = c.repository.GalleryFinder.Find(ctx, galleryID) diff --git a/pkg/scraper/postprocessing.go b/pkg/scraper/postprocessing.go index a7d59605b..cf8cac1eb 100644 --- a/pkg/scraper/postprocessing.go +++ b/pkg/scraper/postprocessing.go @@ -47,7 +47,7 @@ func (c Cache) postScrape(ctx context.Context, content ScrapedContent) (ScrapedC } func (c Cache) postScrapePerformer(ctx context.Context, p models.ScrapedPerformer) (ScrapedContent, error) { - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { tqb := c.repository.TagFinder tags, err := postProcessTags(ctx, tqb, p.Tags) @@ -73,7 +73,7 @@ func (c Cache) postScrapePerformer(ctx context.Context, p models.ScrapedPerforme func (c Cache) postScrapeMovie(ctx context.Context, m models.ScrapedMovie) (ScrapedContent, error) { if m.Studio != nil { - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { return match.ScrapedStudio(ctx, c.repository.StudioFinder, m.Studio, nil) }); err != nil { return nil, err @@ -106,7 +106,7 @@ func (c Cache) postScrapeScenePerformer(ctx context.Context, p models.ScrapedPer } func (c Cache) postScrapeScene(ctx context.Context, scene ScrapedScene) (ScrapedContent, error) { - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { pqb := c.repository.PerformerFinder mqb := c.repository.MovieFinder tqb := c.repository.TagFinder @@ -160,7 +160,7 @@ func (c Cache) postScrapeScene(ctx context.Context, scene ScrapedScene) (Scraped } func (c Cache) postScrapeGallery(ctx context.Context, g ScrapedGallery) (ScrapedContent, error) { - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { pqb := c.repository.PerformerFinder tqb := c.repository.TagFinder sqb := c.repository.StudioFinder diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 869e7b581..a9e3cf54f 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -131,7 +131,7 @@ func (c Client) FindStashBoxSceneByFingerprints(ctx context.Context, sceneID int func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, ids []int) ([][]*scraper.ScrapedScene, error) { var fingerprints [][]*graphql.FingerprintQueryInput - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { qb := c.repository.Scene for _, sceneID := range ids { @@ -245,7 +245,7 @@ func (c Client) SubmitStashBoxFingerprints(ctx context.Context, sceneIDs []strin var fingerprints []graphql.FingerprintSubmission - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { qb := c.repository.Scene for _, sceneID := range ids { @@ -386,7 +386,7 @@ func (c Client) FindStashBoxPerformersByNames(ctx context.Context, performerIDs var performers []*models.Performer - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { qb := c.repository.Performer for _, performerID := range ids { @@ -420,7 +420,7 @@ func (c Client) FindStashBoxPerformersByPerformerNames(ctx context.Context, perf var performers []*models.Performer - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { qb := c.repository.Performer for _, performerID := range ids { @@ -705,7 +705,7 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images) } - if err := txn.WithTxn(ctx, c.txnManager, func(ctx context.Context) error { + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { pqb := c.repository.Performer tqb := c.repository.Tag diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 550c66763..93d7f09db 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "path/filepath" - "sync" "time" "github.com/fvbommel/sortorder" @@ -73,7 +72,7 @@ type Database struct { schemaVersion uint - writeMu sync.Mutex + lockChan chan struct{} } func NewDatabase() *Database { @@ -87,6 +86,7 @@ func NewDatabase() *Database { Image: NewImageStore(fileStore), Gallery: NewGalleryStore(fileStore, folderStore), Performer: NewPerformerStore(), + lockChan: make(chan struct{}, 1), } return ret @@ -106,8 +106,8 @@ func (db *Database) Ready() error { // necessary migrations must be run separately using RunMigrations. // Returns true if the database is new. func (db *Database) Open(dbPath string) error { - db.writeMu.Lock() - defer db.writeMu.Unlock() + db.lockNoCtx() + defer db.unlock() db.dbPath = dbPath @@ -152,9 +152,36 @@ func (db *Database) Open(dbPath string) error { return nil } +// lock locks the database for writing. +// This method will block until the lock is acquired of the context is cancelled. +func (db *Database) lock(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case db.lockChan <- struct{}{}: + return nil + } +} + +// lock locks the database for writing. This method will block until the lock is acquired. +func (db *Database) lockNoCtx() { + db.lockChan <- struct{}{} +} + +// unlock unlocks the database +func (db *Database) unlock() { + // will block the caller if the lock is not held, so check first + select { + case <-db.lockChan: + return + default: + panic("database is not locked") + } +} + func (db *Database) Close() error { - db.writeMu.Lock() - defer db.writeMu.Unlock() + db.lockNoCtx() + defer db.unlock() if db.db != nil { if err := db.db.Close(); err != nil { diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index cb89152e0..d40859de9 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -929,18 +929,15 @@ func Test_imageQueryBuilder_Destroy(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) - withRollbackTxn(func(ctx context.Context) error { - if err := qb.Destroy(ctx, tt.id); (err != nil) != tt.wantErr { - t.Errorf("imageQueryBuilder.Destroy() error = %v, wantErr %v", err, tt.wantErr) - } + if err := qb.Destroy(ctx, tt.id); (err != nil) != tt.wantErr { + t.Errorf("imageQueryBuilder.Destroy() error = %v, wantErr %v", err, tt.wantErr) + } - // ensure cannot be found - i, err := qb.Find(ctx, tt.id) + // ensure cannot be found + i, err := qb.Find(ctx, tt.id) - assert.NotNil(err) - assert.Nil(i) - return nil - }) + assert.NotNil(err) + assert.Nil(i) }) } } diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 16442b7d3..72de32f70 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -1398,18 +1398,15 @@ func Test_sceneQueryBuilder_Destroy(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) - withRollbackTxn(func(ctx context.Context) error { - if err := qb.Destroy(ctx, tt.id); (err != nil) != tt.wantErr { - t.Errorf("sceneQueryBuilder.Destroy() error = %v, wantErr %v", err, tt.wantErr) - } + if err := qb.Destroy(ctx, tt.id); (err != nil) != tt.wantErr { + t.Errorf("sceneQueryBuilder.Destroy() error = %v, wantErr %v", err, tt.wantErr) + } - // ensure cannot be found - i, err := qb.Find(ctx, tt.id) + // ensure cannot be found + i, err := qb.Find(ctx, tt.id) - assert.NotNil(err) - assert.Nil(i) - return nil - }) + assert.NotNil(err) + assert.Nil(i) }) } } @@ -1477,26 +1474,23 @@ func Test_sceneQueryBuilder_Find(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { assert := assert.New(t) - withTxn(func(ctx context.Context) error { - got, err := qb.Find(ctx, tt.id) - if (err != nil) != tt.wantErr { - t.Errorf("sceneQueryBuilder.Find() error = %v, wantErr %v", err, tt.wantErr) - return nil + got, err := qb.Find(ctx, tt.id) + if (err != nil) != tt.wantErr { + t.Errorf("sceneQueryBuilder.Find() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got != nil { + // load relationships + if err := loadSceneRelationships(ctx, *tt.want, got); err != nil { + t.Errorf("loadSceneRelationships() error = %v", err) + return } - if got != nil { - // load relationships - if err := loadSceneRelationships(ctx, *tt.want, got); err != nil { - t.Errorf("loadSceneRelationships() error = %v", err) - return nil - } + clearSceneFileIDs(got) + } - clearSceneFileIDs(got) - } - - assert.Equal(tt.want, got) - return nil - }) + assert.Equal(tt.want, got) }) } } @@ -1620,23 +1614,19 @@ func Test_sceneQueryBuilder_FindByChecksum(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { - withTxn(func(ctx context.Context) error { - assert := assert.New(t) - got, err := qb.FindByChecksum(ctx, tt.checksum) - if (err != nil) != tt.wantErr { - t.Errorf("sceneQueryBuilder.FindByChecksum() error = %v, wantErr %v", err, tt.wantErr) - return nil - } + assert := assert.New(t) + got, err := qb.FindByChecksum(ctx, tt.checksum) + if (err != nil) != tt.wantErr { + t.Errorf("sceneQueryBuilder.FindByChecksum() error = %v, wantErr %v", err, tt.wantErr) + return + } - if err := postFindScenes(ctx, tt.want, got); err != nil { - t.Errorf("loadSceneRelationships() error = %v", err) - return nil - } + if err := postFindScenes(ctx, tt.want, got); err != nil { + t.Errorf("loadSceneRelationships() error = %v", err) + return + } - assert.Equal(tt.want, got) - - return nil - }) + assert.Equal(tt.want, got) }) } } @@ -1694,23 +1684,20 @@ func Test_sceneQueryBuilder_FindByOSHash(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { - withTxn(func(ctx context.Context) error { - got, err := qb.FindByOSHash(ctx, tt.oshash) - if (err != nil) != tt.wantErr { - t.Errorf("sceneQueryBuilder.FindByOSHash() error = %v, wantErr %v", err, tt.wantErr) - return nil - } + got, err := qb.FindByOSHash(ctx, tt.oshash) + if (err != nil) != tt.wantErr { + t.Errorf("sceneQueryBuilder.FindByOSHash() error = %v, wantErr %v", err, tt.wantErr) + return + } - if err := postFindScenes(ctx, tt.want, got); err != nil { - t.Errorf("loadSceneRelationships() error = %v", err) - return nil - } + if err := postFindScenes(ctx, tt.want, got); err != nil { + t.Errorf("loadSceneRelationships() error = %v", err) + return + } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("sceneQueryBuilder.FindByOSHash() = %v, want %v", got, tt.want) - } - return nil - }) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("sceneQueryBuilder.FindByOSHash() = %v, want %v", got, tt.want) + } }) } } @@ -1768,23 +1755,19 @@ func Test_sceneQueryBuilder_FindByPath(t *testing.T) { for _, tt := range tests { runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { - withTxn(func(ctx context.Context) error { - assert := assert.New(t) - got, err := qb.FindByPath(ctx, tt.path) - if (err != nil) != tt.wantErr { - t.Errorf("sceneQueryBuilder.FindByPath() error = %v, wantErr %v", err, tt.wantErr) - return nil - } + assert := assert.New(t) + got, err := qb.FindByPath(ctx, tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("sceneQueryBuilder.FindByPath() error = %v, wantErr %v", err, tt.wantErr) + return + } - if err := postFindScenes(ctx, tt.want, got); err != nil { - t.Errorf("loadSceneRelationships() error = %v", err) - return nil - } + if err := postFindScenes(ctx, tt.want, got); err != nil { + t.Errorf("loadSceneRelationships() error = %v", err) + return + } - assert.Equal(tt.want, got) - - return nil - }) + assert.Equal(tt.want, got) }) } } diff --git a/pkg/sqlite/transaction.go b/pkg/sqlite/transaction.go index c1f81d569..c56b2fcd9 100644 --- a/pkg/sqlite/transaction.go +++ b/pkg/sqlite/transaction.go @@ -17,6 +17,7 @@ type key int const ( txnKey key = iota + 1 dbKey + exclusiveKey ) func (db *Database) WithDatabase(ctx context.Context) (context.Context, error) { @@ -28,7 +29,7 @@ func (db *Database) WithDatabase(ctx context.Context) (context.Context, error) { return context.WithValue(ctx, dbKey, db.db), nil } -func (db *Database) Begin(ctx context.Context) (context.Context, error) { +func (db *Database) Begin(ctx context.Context, exclusive bool) (context.Context, error) { if tx, _ := getTx(ctx); tx != nil { // log the stack trace so we can see logger.Error(string(debug.Stack())) @@ -36,11 +37,23 @@ func (db *Database) Begin(ctx context.Context) (context.Context, error) { return nil, fmt.Errorf("already in transaction") } + if exclusive { + if err := db.lock(ctx); err != nil { + return nil, err + } + } + tx, err := db.db.BeginTxx(ctx, nil) if err != nil { + // begin failed, unlock + if exclusive { + db.unlock() + } return nil, fmt.Errorf("beginning transaction: %w", err) } + ctx = context.WithValue(ctx, exclusiveKey, exclusive) + return context.WithValue(ctx, txnKey, tx), nil } @@ -50,6 +63,8 @@ func (db *Database) Commit(ctx context.Context) error { return err } + defer db.txnComplete(ctx) + if err := tx.Commit(); err != nil { return err } @@ -63,6 +78,8 @@ func (db *Database) Rollback(ctx context.Context) error { return err } + defer db.txnComplete(ctx) + if err := tx.Rollback(); err != nil { return err } @@ -70,6 +87,12 @@ func (db *Database) Rollback(ctx context.Context) error { return nil } +func (db *Database) txnComplete(ctx context.Context) { + if exclusive := ctx.Value(exclusiveKey).(bool); exclusive { + db.unlock() + } +} + func getTx(ctx context.Context) (*sqlx.Tx, error) { tx, ok := ctx.Value(txnKey).(*sqlx.Tx) if !ok || tx == nil { diff --git a/pkg/sqlite/transaction_test.go b/pkg/sqlite/transaction_test.go new file mode 100644 index 000000000..325afa11b --- /dev/null +++ b/pkg/sqlite/transaction_test.go @@ -0,0 +1,243 @@ +//go:build integration +// +build integration + +package sqlite_test + +import ( + "context" + "sync" + "testing" + + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/txn" +) + +// this test is left commented out as it is not deterministic. +// func TestConcurrentExclusiveTxn(t *testing.T) { +// const ( +// workers = 8 +// loops = 100 +// innerLoops = 10 +// sleepTime = 2 * time.Millisecond +// ) +// ctx := context.Background() + +// var wg sync.WaitGroup +// for k := 0; k < workers; k++ { +// wg.Add(1) +// go func(wk int) { +// for l := 0; l < loops; l++ { +// // change this to WithReadTxn to see locked database error +// if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { +// for ll := 0; ll < innerLoops; ll++ { +// scene := &models.Scene{ +// Title: "test", +// } + +// if err := db.Scene.Create(ctx, scene, nil); err != nil { +// return err +// } + +// if err := db.Scene.Destroy(ctx, scene.ID); err != nil { +// return err +// } +// } +// time.Sleep(sleepTime) + +// return nil +// }); err != nil { +// t.Errorf("worker %d loop %d: %v", wk, l, err) +// } +// } + +// wg.Done() +// }(k) +// } + +// wg.Wait() +// } + +func TestConcurrentReadTxn(t *testing.T) { + var wg sync.WaitGroup + ctx := context.Background() + c := make(chan struct{}, 1) + + // first thread + wg.Add(2) + go func() { + defer wg.Done() + if err := txn.WithReadTxn(ctx, db, func(ctx context.Context) error { + scene := &models.Scene{ + Title: "test", + } + + if err := db.Scene.Create(ctx, scene, nil); err != nil { + return err + } + + // wait for other thread to start + c <- struct{}{} + <-c + + if err := db.Scene.Destroy(ctx, scene.ID); err != nil { + return err + } + + return nil + }); err != nil { + t.Errorf("unexpected error in first thread: %v", err) + } + }() + + // second thread + go func() { + defer wg.Done() + _ = txn.WithReadTxn(ctx, db, func(ctx context.Context) error { + // wait for first thread + <-c + defer func() { + c <- struct{}{} + }() + + scene := &models.Scene{ + Title: "test", + } + + // expect error when we try to do this, as the other thread has already + // modified this table + if err := db.Scene.Create(ctx, scene, nil); err != nil { + if !db.IsLocked(err) { + t.Errorf("unexpected error: %v", err) + } + return err + } else { + t.Errorf("expected locked error in second thread") + } + + return nil + }) + }() + + wg.Wait() +} + +func TestConcurrentExclusiveAndReadTxn(t *testing.T) { + var wg sync.WaitGroup + ctx := context.Background() + c := make(chan struct{}, 1) + + // first thread + wg.Add(2) + go func() { + defer wg.Done() + if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { + scene := &models.Scene{ + Title: "test", + } + + if err := db.Scene.Create(ctx, scene, nil); err != nil { + return err + } + + // wait for other thread to start + c <- struct{}{} + <-c + + if err := db.Scene.Destroy(ctx, scene.ID); err != nil { + return err + } + + return nil + }); err != nil { + t.Errorf("unexpected error in first thread: %v", err) + } + }() + + // second thread + go func() { + defer wg.Done() + _ = txn.WithReadTxn(ctx, db, func(ctx context.Context) error { + // wait for first thread + <-c + defer func() { + c <- struct{}{} + }() + + if _, err := db.Scene.Find(ctx, sceneIDs[sceneIdx1WithPerformer]); err != nil { + t.Errorf("unexpected error: %v", err) + return err + } + + return nil + }) + }() + + wg.Wait() +} + +// this test is left commented out as it is not deterministic. +// func TestConcurrentExclusiveAndReadTxns(t *testing.T) { +// const ( +// writeWorkers = 4 +// readWorkers = 4 +// loops = 200 +// innerLoops = 10 +// sleepTime = 1 * time.Millisecond +// ) +// ctx := context.Background() + +// var wg sync.WaitGroup +// for k := 0; k < writeWorkers; k++ { +// wg.Add(1) +// go func(wk int) { +// for l := 0; l < loops; l++ { +// if err := txn.WithTxn(ctx, db, func(ctx context.Context) error { +// for ll := 0; ll < innerLoops; ll++ { +// scene := &models.Scene{ +// Title: "test", +// } + +// if err := db.Scene.Create(ctx, scene, nil); err != nil { +// return err +// } + +// if err := db.Scene.Destroy(ctx, scene.ID); err != nil { +// return err +// } +// } +// time.Sleep(sleepTime) + +// return nil +// }); err != nil { +// t.Errorf("write worker %d loop %d: %v", wk, l, err) +// } +// } + +// wg.Done() +// }(k) +// } + +// for k := 0; k < readWorkers; k++ { +// wg.Add(1) +// go func(wk int) { +// for l := 0; l < loops; l++ { +// if err := txn.WithReadTxn(ctx, db, func(ctx context.Context) error { +// for ll := 0; ll < innerLoops; ll++ { +// if _, err := db.Scene.Find(ctx, sceneIDs[ll%totalScenes]); err != nil { +// return err +// } +// } +// time.Sleep(sleepTime) + +// return nil +// }); err != nil { +// t.Errorf("read worker %d loop %d: %v", wk, l, err) +// } +// } + +// wg.Done() +// }(k) +// } + +// wg.Wait() +// } diff --git a/pkg/txn/transaction.go b/pkg/txn/transaction.go index 0a0390382..2a78da721 100644 --- a/pkg/txn/transaction.go +++ b/pkg/txn/transaction.go @@ -6,7 +6,7 @@ import ( ) type Manager interface { - Begin(ctx context.Context) (context.Context, error) + Begin(ctx context.Context, exclusive bool) (context.Context, error) Commit(ctx context.Context) error Rollback(ctx context.Context) error @@ -17,18 +17,43 @@ type DatabaseProvider interface { WithDatabase(ctx context.Context) (context.Context, error) } +type DatabaseProviderManager interface { + DatabaseProvider + Manager +} + type TxnFunc func(ctx context.Context) error // WithTxn executes fn in a transaction. If fn returns an error then // the transaction is rolled back. Otherwise it is committed. +// Transaction is exclusive. Only one thread may run a transaction +// using this function at a time. This function will wait until the +// lock is available before executing. +// This function should be used for making changes to the database. func WithTxn(ctx context.Context, m Manager, fn TxnFunc) error { - const execComplete = true - return withTxn(ctx, m, fn, execComplete) + const ( + execComplete = true + exclusive = true + ) + return withTxn(ctx, m, fn, exclusive, execComplete) } -func withTxn(ctx context.Context, m Manager, fn TxnFunc, execCompleteOnLocked bool) error { +// WithReadTxn executes fn in a transaction. If fn returns an error then +// the transaction is rolled back. Otherwise it is committed. +// Transaction is not exclusive and does not enforce read-only restrictions. +// Multiple threads can run transactions using this function concurrently, +// but concurrent writes may result in locked database error. +func WithReadTxn(ctx context.Context, m Manager, fn TxnFunc) error { + const ( + execComplete = true + exclusive = false + ) + return withTxn(ctx, m, fn, exclusive, execComplete) +} + +func withTxn(ctx context.Context, m Manager, fn TxnFunc, exclusive bool, execCompleteOnLocked bool) error { var err error - ctx, err = begin(ctx, m) + ctx, err = begin(ctx, m, exclusive) if err != nil { return err } @@ -59,9 +84,9 @@ func withTxn(ctx context.Context, m Manager, fn TxnFunc, execCompleteOnLocked bo return err } -func begin(ctx context.Context, m Manager) (context.Context, error) { +func begin(ctx context.Context, m Manager, exclusive bool) (context.Context, error) { var err error - ctx, err = m.Begin(ctx) + ctx, err = m.Begin(ctx, exclusive) if err != nil { return nil, err } @@ -102,6 +127,9 @@ func WithDatabase(ctx context.Context, p DatabaseProvider, fn TxnFunc) error { return fn(ctx) } +// Retryer is a provides WithTxn function that retries the transaction +// if it fails with a locked database error. +// Transactions are run in exclusive mode. type Retryer struct { Manager Manager // use value < 0 to retry forever @@ -113,8 +141,11 @@ func (r Retryer) WithTxn(ctx context.Context, fn TxnFunc) error { var attempt int var err error for attempt = 1; attempt <= r.Retries || r.Retries < 0; attempt++ { - const execComplete = false - err = withTxn(ctx, r.Manager, fn, execComplete) + const ( + execComplete = false + exclusive = true + ) + err = withTxn(ctx, r.Manager, fn, exclusive, execComplete) if err == nil { return nil diff --git a/ui/v2.5/src/docs/en/Changelog/v0180.md b/ui/v2.5/src/docs/en/Changelog/v0180.md index 8091e6caa..740e0caa8 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0180.md +++ b/ui/v2.5/src/docs/en/Changelog/v0180.md @@ -16,6 +16,7 @@ * Changed Performer height to be numeric, and changed filtering accordingly. ([#3060](https://github.com/stashapp/stash/pull/3060)) ### 🐛 Bug fixes +* Fixed `database is locked` errors when performing operations while running a scan. ([#3153](https://github.com/stashapp/stash/pull/3153)) * Fixed database backup in incorrect directory during migration when database location is an absolute path. ([#3140](https://github.com/stashapp/stash/pull/3140)) * Fixed gallery create post hook not being fired during gallery creation. ([#3134](https://github.com/stashapp/stash/pull/3134)) * Fixed autotag error when tagging a large amount of objects. ([#3106](https://github.com/stashapp/stash/pull/3106))