Repository: kopia/kopia
Pull request #2527 (2022-10-28 05:49:31)

refactor(general): introduced generics to reduce boilerplate code
This removes tons of boilerplate code around:

- retry loop
- connection management
- storage registration


codecov[bot]: # [Codecov](https://codecov.io/gh/kopia/kopia/pull/2527?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.46**% // Head: **74.47**% // Increases project coverage by **`+0.00%`** :tada:
> Coverage data is based on head [(`9b66307`)](https://codecov.io/gh/kopia/kopia/pull/2527?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`ec5fe21`)](https://codecov.io/gh/kopia/kopia/commit/ec5fe211cf906bb51936c75eb05ce7f69236f813?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Patch coverage: 85.55% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #2527   +/-   ##
=======================================
  Coverage   74.46%   74.47%           
=======================================
  Files         442      442           
  Lines       34979    34928   -51     
=======================================
- Hits        26048    26012   -36     
+ Misses       7041     7030   -11     
+ Partials     1890     1886    -4     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2527?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [cli/app.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2FwcC5nbw==) | `76.69% <ø> (ø)` | |
| [cli/command\_blob\_stats.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfYmxvYl9zdGF0cy5nbw==) | `8.95% <ø> (+0.13%)` | :arrow_up: |
| [cli/command\_content\_stats.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfY29udGVudF9zdGF0cy5nbw==) | `90.19% <0.00%> (ø)` | |
| [cli/command\_logs\_session.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfbG9nc19zZXNzaW9uLmdv) | `71.91% <ø> (-0.32%)` | :arrow_down: |
| [cli/command\_policy\_set.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfcG9saWN5X3NldC5nbw==) | `64.51% <ø> (ø)` | |
| [cli/observability\_flags.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL29ic2VydmFiaWxpdHlfZmxhZ3MuZ28=) | `77.16% <0.00%> (ø)` | |
| [cli/storage\_azure.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL3N0b3JhZ2VfYXp1cmUuZ28=) | `75.00% <0.00%> (ø)` | |
| [cli/storage\_b2.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL3N0b3JhZ2VfYjIuZ28=) | `66.66% <0.00%> (ø)` | |
| [cli/storage\_filesystem.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL3N0b3JhZ2VfZmlsZXN5c3RlbS5nbw==) | `66.66% <ø> (+0.62%)` | :arrow_up: |
| [cli/storage\_gcs.go](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL3N0b3JhZ2VfZ2NzLmdv) | `47.05% <0.00%> (ø)` | |
| ... and [40 more](https://codecov.io/gh/kopia/kopia/pull/2527/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/kopia/kopia/pull/2527?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 052e3637..a33d726a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -143,6 +143,12 @@ issues:
     - text: "Line contains TODO"
       linters:
         - godox
+    - text: "error returned from external package is unwrapped: sig: func github.com/kopia/kopia/internal/retry"
+      linters:
+        - wrapcheck
+    - text: "error returned from external package is unwrapped: sig: func github.com/kopia/kopia/internal/connection.UsingConnection"
+      linters:
+        - wrapcheck
     - text: timeCmpSimplify
       linters:
         - gocritic
diff --git a/cli/command_benchmark.go b/cli/command_benchmark.go
index a0d2175c..30190641 100644
--- a/cli/command_benchmark.go
+++ b/cli/command_benchmark.go
@@ -30,7 +30,14 @@ type cryptoBenchResult struct {
 	throughput float64
 }
 
-func runInParallel(parallel int, run func() interface{}) interface{} {
+func runInParallelNoResult(parallel int, run func()) {
+	runInParallel(parallel, func() any {
+		run()
+		return nil
+	})
+}
+
+func runInParallel[T any](parallel int, run func() T) T {
 	var wg sync.WaitGroup
 
 	for i := 0; i < parallel-1; i++ {
diff --git a/cli/command_benchmark_compression.go b/cli/command_benchmark_compression.go
index b2c62f13..4e80c43a 100644
--- a/cli/command_benchmark_compression.go
+++ b/cli/command_benchmark_compression.go
@@ -121,7 +121,7 @@ func (c *commandBenchmarkCompression) run(ctx context.Context) error {
 
 		var startMS, endMS runtime.MemStats
 
-		run := func() interface{} {
+		run := func() int64 {
 			var (
 				compressedSize int64
 				lastHash       uint64
@@ -159,7 +159,7 @@ func (c *commandBenchmarkCompression) run(ctx context.Context) error {
 
 		runtime.ReadMemStats(&startMS)
 
-		compressedSize, _ := runInParallel(c.parallel, run).(int64)
+		compressedSize := runInParallel(c.parallel, run)
 
 		runtime.ReadMemStats(&endMS)
 
diff --git a/cli/command_benchmark_crypto.go b/cli/command_benchmark_crypto.go
index 8099687a..6ad1861a 100644
--- a/cli/command_benchmark_crypto.go
+++ b/cli/command_benchmark_crypto.go
@@ -91,7 +91,7 @@ func (c *commandBenchmarkCrypto) runBenchmark(ctx context.Context) []cryptoBench
 
 			hashCount := c.repeat
 
-			runInParallel(c.parallel, func() interface{} {
+			runInParallelNoResult(c.parallel, func() {
 				var hashOutput [hashing.MaxHashSize]byte
 
 				var encryptOutput gather.WriteBuffer
@@ -105,8 +105,6 @@ func (c *commandBenchmarkCrypto) runBenchmark(ctx context.Context) []cryptoBench
 						break
 					}
 				}
-
-				return nil
 			})
 
 			_, bytesPerSecond := tt.Completed(float64(c.parallel) * float64(len(data)) * float64(hashCount))
diff --git a/cli/command_benchmark_ecc.go b/cli/command_benchmark_ecc.go
index f78c7ffe..b9ef7b7f 100644
--- a/cli/command_benchmark_ecc.go
+++ b/cli/command_benchmark_ecc.go
@@ -95,7 +95,7 @@ func (c *commandBenchmarkEcc) runBenchmark(ctx context.Context) []eccBenchResult
 
 			repeat := c.repeat
 
-			runInParallel(c.parallel, func() interface{} {
+			runInParallelNoResult(c.parallel, func() {
 				var tmp gather.WriteBuffer
 				defer tmp.Close()
 
@@ -105,8 +105,6 @@ func (c *commandBenchmarkEcc) runBenchmark(ctx context.Context) []eccBenchResult
 						break
 					}
 				}
-
-				return nil
 			})
 
 			_, bytesPerSecondEncoding := tt.Completed(float64(c.parallel) * float64(len(data)) * float64(repeat))
@@ -123,7 +121,7 @@ func (c *commandBenchmarkEcc) runBenchmark(ctx context.Context) []eccBenchResult
 			input = encodedBuffer.Bytes()
 			tt = timetrack.Start()
 
-			runInParallel(c.parallel, func() interface{} {
+			runInParallelNoResult(c.parallel, func() {
 				var tmp gather.WriteBuffer
 				defer tmp.Close()
 
@@ -133,8 +131,6 @@ func (c *commandBenchmarkEcc) runBenchmark(ctx context.Context) []eccBenchResult
 						break
 					}
 				}
-
-				return nil
 			})
 
 			_, bytesPerSecondDecoding := tt.Completed(float64(c.parallel) * float64(len(data)) * float64(repeat))
diff --git a/cli/command_benchmark_encryption.go b/cli/command_benchmark_encryption.go
index d583a989..4b870d45 100644
--- a/cli/command_benchmark_encryption.go
+++ b/cli/command_benchmark_encryption.go
@@ -83,7 +83,7 @@ func (c *commandBenchmarkEncryption) runBenchmark(ctx context.Context) []cryptoB
 
 		hashCount := c.repeat
 
-		runInParallel(c.parallel, func() interface{} {
+		runInParallelNoResult(c.parallel, func() {
 			var hashOutput [hashing.MaxHashSize]byte
 
 			var encryptOutput gather.WriteBuffer
@@ -95,8 +95,6 @@ func (c *commandBenchmarkEncryption) runBenchmark(ctx context.Context) []cryptoB
 					break
 				}
 			}
-
-			return nil
 		})
 
 		_, bytesPerSecond := tt.Completed(float64(c.parallel) * float64(len(data)) * float64(hashCount))
diff --git a/cli/command_benchmark_hashing.go b/cli/command_benchmark_hashing.go
index ec2b4008..1c071f66 100644
--- a/cli/command_benchmark_hashing.go
+++ b/cli/command_benchmark_hashing.go
@@ -78,14 +78,12 @@ func (c *commandBenchmarkHashing) runBenchmark(ctx context.Context) []cryptoBenc
 
 		hashCount := c.repeat
 
-		runInParallel(c.parallel, func() interface{} {
+		runInParallelNoResult(c.parallel, func() {
 			var hashOutput [hashing.MaxHashSize]byte
 
 			for i := 0; i < hashCount; i++ {
 				hf(hashOutput[:0], input)
 			}
-
-			return nil
 		})
 
 		_, bytesPerSecond := tt.Completed(float64(c.parallel) * float64(len(data)) * float64(hashCount))
diff --git a/cli/command_benchmark_splitters.go b/cli/command_benchmark_splitters.go
index 5d9689ca..99b505a8 100644
--- a/cli/command_benchmark_splitters.go
+++ b/cli/command_benchmark_splitters.go
@@ -80,7 +80,7 @@ type benchResult struct {
 	for _, sp := range splitter.SupportedAlgorithms() {
 		tt := timetrack.Start()
 
-		segmentLengths, _ := runInParallel(c.parallel, func() interface{} {
+		segmentLengths := runInParallel(c.parallel, func() []int {
 			fact := splitter.GetFactory(sp)
 
 			var segmentLengths []int
@@ -102,7 +102,7 @@ type benchResult struct {
 			}
 
 			return segmentLengths
-		}).([]int)
+		})
 
 		_, bytesPerSecond := tt.Completed(float64(c.parallel) * float64(c.blockCount) * float64(c.blockSize))
 
diff --git a/cli/storage_azure.go b/cli/storage_azure.go
index fd6bc30b..200853aa 100644
--- a/cli/storage_azure.go
+++ b/cli/storage_azure.go
@@ -26,5 +26,5 @@ func (c *storageAzureFlags) Setup(svc StorageProviderServices, cmd *kingpin.CmdC
 
 func (c *storageAzureFlags) Connect(ctx context.Context, isCreate bool, formatVersion int) (blob.Storage, error) {
 	//nolint:wrapcheck
-	return azure.New(ctx, &c.azOptions)
+	return azure.New(ctx, &c.azOptions, false)
 }
diff --git a/cli/storage_b2.go b/cli/storage_b2.go
index f7d6ff07..af9595bf 100644
--- a/cli/storage_b2.go
+++ b/cli/storage_b2.go
@@ -23,5 +23,5 @@ func (c *storageB2Flags) Setup(svc StorageProviderServices, cmd *kingpin.CmdClau
 
 func (c *storageB2Flags) Connect(ctx context.Context, isCreate bool, formatVersion int) (blob.Storage, error) {
 	//nolint:wrapcheck
-	return b2.New(ctx, &c.b2options)
+	return b2.New(ctx, &c.b2options, false)
 }
diff --git a/cli/storage_gcs.go b/cli/storage_gcs.go
index b679194e..2c5850aa 100644
--- a/cli/storage_gcs.go
+++ b/cli/storage_gcs.go
@@ -40,5 +40,5 @@ func (c *storageGCSFlags) Connect(ctx context.Context, isCreate bool, formatVers
 	}
 
 	//nolint:wrapcheck
-	return gcs.New(ctx, &c.options)
+	return gcs.New(ctx, &c.options, false)
 }
diff --git a/cli/storage_gdrive.go b/cli/storage_gdrive.go
index d6dddbff..d4ecfad0 100644
--- a/cli/storage_gdrive.go
+++ b/cli/storage_gdrive.go
@@ -39,5 +39,5 @@ func (c *storageGDriveFlags) Connect(ctx context.Context, isCreate bool, formatV
 	}
 
 	//nolint:wrapcheck
-	return gdrive.New(ctx, &c.options)
+	return gdrive.New(ctx, &c.options, false)
 }
diff --git a/cli/storage_s3.go b/cli/storage_s3.go
index ebe4026e..edadd8fb 100644
--- a/cli/storage_s3.go
+++ b/cli/storage_s3.go
@@ -52,5 +52,5 @@ func (c *storageS3Flags) Connect(ctx context.Context, isCreate bool, formatVersi
 	}
 
 	//nolint:wrapcheck
-	return s3.New(ctx, &c.s3options)
+	return s3.New(ctx, &c.s3options, false)
 }
diff --git a/fs/localfs/local_fs_pool.go b/fs/localfs/local_fs_pool.go
index 06268129..a75b5a39 100644
--- a/fs/localfs/local_fs_pool.go
+++ b/fs/localfs/local_fs_pool.go
@@ -4,53 +4,16 @@
 
 //nolint:gochecknoglobals
 var (
-	filesystemFilePool = freepool.New(
-		func() interface{} { return &filesystemFile{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*filesystemFile) = filesystemFile{}
-		},
-	)
-	filesystemDirectoryPool = freepool.New(
-		func() interface{} { return &filesystemDirectory{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*filesystemDirectory) = filesystemDirectory{}
-		},
-	)
-	filesystemSymlinkPool = freepool.New(
-		func() interface{} { return &filesystemSymlink{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*filesystemSymlink) = filesystemSymlink{}
-		},
-	)
-	filesystemErrorEntryPool = freepool.New(
-		func() interface{} { return &filesystemErrorEntry{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*filesystemErrorEntry) = filesystemErrorEntry{}
-		},
-	)
-	shallowFilesystemFilePool = freepool.New(
-		func() interface{} { return &shallowFilesystemFile{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*shallowFilesystemFile) = shallowFilesystemFile{}
-		},
-	)
-	shallowFilesystemDirectoryPool = freepool.New(
-		func() interface{} { return &shallowFilesystemDirectory{} },
-		func(v interface{}) {
-			//nolint:forcetypeassert
-			*v.(*shallowFilesystemDirectory) = shallowFilesystemDirectory{}
-		},
-	)
+	filesystemFilePool             = freepool.NewStruct(filesystemFile{})
+	filesystemDirectoryPool        = freepool.NewStruct(filesystemDirectory{})
+	filesystemSymlinkPool          = freepool.NewStruct(filesystemSymlink{})
+	filesystemErrorEntryPool       = freepool.NewStruct(filesystemErrorEntry{})
+	shallowFilesystemFilePool      = freepool.NewStruct(shallowFilesystemFile{})
+	shallowFilesystemDirectoryPool = freepool.NewStruct(shallowFilesystemDirectory{})
 )
 
 func newFilesystemFile(e filesystemEntry) *filesystemFile {
-	//nolint:forcetypeassert
-	fsf := filesystemFilePool.Take().(*filesystemFile)
+	fsf := filesystemFilePool.Take()
 	fsf.filesystemEntry = e
 
 	return fsf
@@ -61,8 +24,7 @@ func (fsf *filesystemFile) Close() {
 }
 
 func newFilesystemDirectory(e filesystemEntry) *filesystemDirectory {
-	//nolint:forcetypeassert
-	fsd := filesystemDirectoryPool.Take().(*filesystemDirectory)
+	fsd := filesystemDirectoryPool.Take()
 	fsd.filesystemEntry = e
 
 	return fsd
@@ -73,8 +35,7 @@ func (fsd *filesystemDirectory) Close() {
 }
 
 func newFilesystemSymlink(e filesystemEntry) *filesystemSymlink {
-	//nolint:forcetypeassert
-	fsd := filesystemSymlinkPool.Take().(*filesystemSymlink)
+	fsd := filesystemSymlinkPool.Take()
 	fsd.filesystemEntry = e
 
 	return fsd
@@ -85,8 +46,7 @@ func (fsl *filesystemSymlink) Close() {
 }
 
 func newFilesystemErrorEntry(e filesystemEntry, err error) *filesystemErrorEntry {
-	//nolint:forcetypeassert
-	fse := filesystemErrorEntryPool.Take().(*filesystemErrorEntry)
+	fse := filesystemErrorEntryPool.Take()
 	fse.filesystemEntry = e
 	fse.err = err
 
@@ -98,8 +58,7 @@ func (e *filesystemErrorEntry) Close() {
 }
 
 func newShallowFilesystemFile(e filesystemEntry) *shallowFilesystemFile {
-	//nolint:forcetypeassert
-	fsf := shallowFilesystemFilePool.Take().(*shallowFilesystemFile)
+	fsf := shallowFilesystemFilePool.Take()
 	fsf.filesystemEntry = e
 
 	return fsf
@@ -110,8 +69,7 @@ func (fsf *shallowFilesystemFile) Close() {
 }
 
 func newShallowFilesystemDirectory(e filesystemEntry) *shallowFilesystemDirectory {
-	//nolint:forcetypeassert
-	fsf := shallowFilesystemDirectoryPool.Take().(*shallowFilesystemDirectory)
+	fsf := shallowFilesystemDirectoryPool.Take()
 	fsf.filesystemEntry = e
 
 	return fsf
diff --git a/internal/connection/reconnector.go b/internal/connection/reconnector.go
index ccc0ca15..27e23333 100644
--- a/internal/connection/reconnector.go
+++ b/internal/connection/reconnector.go
@@ -56,9 +56,10 @@ func (r *Reconnector) GetOrOpenConnection(ctx context.Context) (Connection, erro
 }
 
 // UsingConnection invokes the provided callback for a Connection.
-func (r *Reconnector) UsingConnection(ctx context.Context, desc string, cb func(cli Connection) (interface{}, error)) (interface{}, error) {
-	//nolint:wrapcheck
-	return retry.WithExponentialBackoff(ctx, desc, func() (interface{}, error) {
+func UsingConnection[T any](ctx context.Context, r *Reconnector, desc string, cb func(cli Connection) (T, error)) (T, error) {
+	var defaultT T
+
+	return retry.WithExponentialBackoff(ctx, desc, func() (T, error) {
 		conn, err := r.GetOrOpenConnection(ctx)
 		if err != nil {
 			if r.connector.IsConnectionClosedError(err) {
@@ -67,7 +68,7 @@ func (r *Reconnector) UsingConnection(ctx context.Context, desc string, cb func(
 
 			r.CloseActiveConnection(ctx)
 
-			return nil, errors.Wrap(err, "error opening connection")
+			return defaultT, errors.Wrap(err, "error opening connection")
 		}
 
 		v, err := cb(conn)
@@ -85,8 +86,8 @@ func (r *Reconnector) UsingConnection(ctx context.Context, desc string, cb func(
 
 // UsingConnectionNoResult invokes the provided callback for a Connection.
 func (r *Reconnector) UsingConnectionNoResult(ctx context.Context, desc string, cb func(cli Connection) error) error {
-	_, err := r.UsingConnection(ctx, desc, func(cli Connection) (interface{}, error) {
-		return nil, cb(cli)
+	_, err := UsingConnection(ctx, r, desc, func(cli Connection) (bool, error) {
+		return true, cb(cli)
 	})
 
 	return err
diff --git a/internal/freepool/freepool.go b/internal/freepool/freepool.go
index c4e7f8a4..ab80969c 100644
--- a/internal/freepool/freepool.go
+++ b/internal/freepool/freepool.go
@@ -5,30 +5,42 @@
 	"sync"
 )
 
-// Pool is a small pool of recently returned objects.
-// Unlike sync.Pool, the pool is not subject to gargbage collection under memory pressure.
-type Pool struct {
-	clean func(v interface{})
+// Pool is a small pool of recently returned objects with built-in cleanup.
+type Pool[T any] struct {
+	clean func(v *T)
 	pool  sync.Pool
 }
 
 // Take returns an item from the pool, and if not available makes a new one.
-func (p *Pool) Take() interface{} {
-	return p.pool.Get()
+func (p *Pool[T]) Take() *T {
+	//nolint:forcetypeassert
+	return p.pool.Get().(*T)
 }
 
 // Return returns an item to the pool after cleaning it.
-func (p *Pool) Return(v interface{}) {
+func (p *Pool[T]) Return(v *T) {
 	p.clean(v)
 	p.pool.Put(v)
 }
 
 // New returns a new free pool.
-func New(makeNew func() interface{}, clean func(v interface{})) *Pool {
-	return &Pool{
+func New[T any](makeNew func() *T, clean func(v *T)) *Pool[T] {
+	return &Pool[T]{
 		clean: clean,
 		pool: sync.Pool{
-			New: makeNew,
+			New: func() any {
+				return makeNew()
+			},
 		},
 	}
 }
+
+// NewStruct returns a pool that produces provided clean structures.
+func NewStruct[T any](cleanItem T) *Pool[T] {
+	return New(func() *T {
+		r := cleanItem
+		return &r
+	}, func(v *T) {
+		*v = cleanItem
+	})
+}
diff --git a/internal/retry/retry.go b/internal/retry/retry.go
index 91964554..04b4509b 100644
--- a/internal/retry/retry.go
+++ b/internal/retry/retry.go
@@ -21,35 +21,32 @@
 
 const retryExponent = 1.5
 
-// AttemptFunc performs an attempt and returns a value (optional, may be nil) and an error.
-type AttemptFunc func() (interface{}, error)
-
 // IsRetriableFunc is a function that determines whether an error is retriable.
 type IsRetriableFunc func(err error) bool
 
 // WithExponentialBackoff runs the provided attempt until it succeeds, retrying on all errors that are
 // deemed retriable by the provided function. The delay between retries grows exponentially up to
 // a certain limit.
-func WithExponentialBackoff(ctx context.Context, desc string, attempt AttemptFunc, isRetriableError IsRetriableFunc) (interface{}, error) {
+func WithExponentialBackoff[T any](ctx context.Context, desc string, attempt func() (T, error), isRetriableError IsRetriableFunc) (T, error) {
 	return internalRetry(ctx, desc, attempt, isRetriableError, retryInitialSleepAmount, retryMaxSleepAmount, maxAttempts, retryExponent)
 }
 
 // WithExponentialBackoffMaxRetries is the same as WithExponentialBackoff,
 // additionally it allows customizing the max number of retries before giving
 // up (count parameter). A negative value for count would run this forever.
-func WithExponentialBackoffMaxRetries(ctx context.Context, count int, desc string, attempt AttemptFunc, isRetriableError IsRetriableFunc) (interface{}, error) {
+func WithExponentialBackoffMaxRetries[T any](ctx context.Context, count int, desc string, attempt func() (T, error), isRetriableError IsRetriableFunc) (T, error) {
 	return internalRetry(ctx, desc, attempt, isRetriableError, retryInitialSleepAmount, retryMaxSleepAmount, count, retryExponent)
 }
 
 // Periodically runs the provided attempt until it succeeds, waiting given fixed amount between attempts.
-func Periodically(ctx context.Context, interval time.Duration, count int, desc string, attempt AttemptFunc, isRetriableError IsRetriableFunc) (interface{}, error) {
+func Periodically[T any](ctx context.Context, interval time.Duration, count int, desc string, attempt func() (T, error), isRetriableError IsRetriableFunc) (T, error) {
 	return internalRetry(ctx, desc, attempt, isRetriableError, interval, interval, count, 1)
 }
 
 // PeriodicallyNoValue runs the provided attempt until it succeeds, waiting given fixed amount between attempts.
 func PeriodicallyNoValue(ctx context.Context, interval time.Duration, count int, desc string, attempt func() error, isRetriableError IsRetriableFunc) error {
-	_, err := Periodically(ctx, interval, count, desc, func() (interface{}, error) {
-		return nil, attempt()
+	_, err := Periodically(ctx, interval, count, desc, func() (bool, error) {
+		return true, attempt()
 	}, isRetriableError)
 
 	return err
@@ -58,7 +55,7 @@ func PeriodicallyNoValue(ctx context.Context, interval time.Duration, count int,
 // internalRetry runs the provided attempt until it succeeds, retrying on all errors that are
 // deemed retriable by the provided function. The delay between retries grows exponentially up to
 // a certain limit.
-func internalRetry(ctx context.Context, desc string, attempt AttemptFunc, isRetriableError IsRetriableFunc, initial, max time.Duration, count int, factor float64) (interface{}, error) {
+func internalRetry[T any](ctx context.Context, desc string, attempt func() (T, error), isRetriableError IsRetriableFunc, initial, max time.Duration, count int, factor float64) (T, error) {
 	sleepAmount := initial
 
 	var (
@@ -66,10 +63,12 @@ func internalRetry(ctx context.Context, desc string, attempt AttemptFunc, isRetr
 		i         = 0
 	)
 
+	var defaultT T
+
 	for ; i < count || count < 0; i++ {
 		if cerr := ctx.Err(); cerr != nil {
 			//nolint:wrapcheck
-			return nil, cerr
+			return defaultT, cerr
 		}
 
 		v, err := attempt()
@@ -92,7 +91,7 @@ func internalRetry(ctx context.Context, desc string, attempt AttemptFunc, isRetr
 		}
 	}
 
-	return nil, errors.Wrapf(lastError, "unable to complete %v despite %v retries", desc, i)
+	return defaultT, errors.Wrapf(lastError, "unable to complete %v despite %v retries", desc, i)
 }
 
 // WithExponentialBackoffNoValue is a shorthand for WithExponentialBackoff except the
diff --git a/internal/workshare/workshare_pool.go b/internal/workshare/workshare_pool.go
index f122b1f7..6ce4d8a8 100644
--- a/internal/workshare/workshare_pool.go
+++ b/internal/workshare/workshare_pool.go
@@ -9,42 +9,42 @@
 // ProcessFunc processes the provided request, which is typically a pointer to a structure.
 // The result will typically be written within the request structure. To avoid allocations,
 // the function should not be a local closure but a named function (either global or a method).
-type ProcessFunc func(c *Pool, request interface{})
+type ProcessFunc[T any] func(c *Pool[T], request T)
 
-type workItem struct {
-	process ProcessFunc     // function to call
-	request interface{}     // parameter to the function
+type workItem[T any] struct {
+	process ProcessFunc[T]  // function to call
+	request T               // parameter to the function
 	wg      *sync.WaitGroup // signal the provided WaitGroup after work is done
 }
 
 // Pool manages a pool of generic workers that can process workItem.
-type Pool struct {
+type Pool[T any] struct {
 	// +checkatomic
 	activeWorkers int32
 
 	semaphore chan struct{}
 
-	work   chan workItem
+	work   chan workItem[T]
 	closed chan struct{}
 
 	wg sync.WaitGroup
 }
 
 // ActiveWorkers returns the number of active workers.
-func (w *Pool) ActiveWorkers() int {
+func (w *Pool[T]) ActiveWorkers() int {
 	return int(atomic.LoadInt32(&w.activeWorkers))
 }
 
 // NewPool creates a worker pool that launches a given number of goroutines that can invoke shared work.
-func NewPool(numWorkers int) *Pool {
+func NewPool[T any](numWorkers int) *Pool[T] {
 	if numWorkers < 0 {
 		numWorkers = 0
 	}
 
-	w := &Pool{
+	w := &Pool[T]{
 		// channel must be unbuffered so that it has exactly as many slots as there are goroutines capable of reading from it
 		// this way by pushing to the channel we can be sure that a pre-spun goroutine will pick it up soon.
-		work:      make(chan workItem),
+		work:      make(chan workItem[T]),
 		closed:    make(chan struct{}),
 		semaphore: make(chan struct{}, numWorkers),
 	}
@@ -75,7 +75,7 @@ func NewPool(numWorkers int) *Pool {
 }
 
 // Close closes the worker pool.
-func (w *Pool) Close() {
+func (w *Pool[T]) Close() {
 	close(w.closed)
 	w.wg.Wait()
 }
diff --git a/internal/workshare/workshare_waitgroup.go b/internal/workshare/workshare_waitgroup.go
index 70409c46..cc2fa369 100644
--- a/internal/workshare/workshare_waitgroup.go
+++ b/internal/workshare/workshare_waitgroup.go
@@ -50,15 +50,15 @@
 // It provides API designed to minimize allocations while being reasonably easy to use.
 // AsyncGroup is very lightweight and NOT safe for concurrent use, all interactions must
 // be from the same goroutine.
-type AsyncGroup struct {
+type AsyncGroup[T any] struct {
 	wg       *sync.WaitGroup
-	requests []interface{}
+	requests []T
 	waited   bool
 	isClosed bool
 }
 
 // Wait waits for scheduled asynchronous work to complete and returns all asynchronously processed inputs.
-func (g *AsyncGroup) Wait() []interface{} {
+func (g *AsyncGroup[T]) Wait() []T {
 	if g.isClosed {
 		panic("Wait() can't be called after Close()")
 	}
@@ -79,7 +79,7 @@ func (g *AsyncGroup) Wait() []interface{} {
 }
 
 // Close ensures all asynchronous work has been awaited for.
-func (g *AsyncGroup) Close() {
+func (g *AsyncGroup[T]) Close() {
 	if g.isClosed {
 		return
 	}
@@ -93,19 +93,17 @@ func (g *AsyncGroup) Close() {
 
 // RunAsync starts the asynchronous work to process the provided request,
 // the user must call Wait() after all RunAsync() have been scheduled.
-func (g *AsyncGroup) RunAsync(w *Pool, process ProcessFunc, request interface{}) {
+func (g *AsyncGroup[T]) RunAsync(w *Pool[T], process ProcessFunc[T], request T) {
 	if g.wg == nil {
 		g.wg = &sync.WaitGroup{}
 	}
 
 	g.wg.Add(1)
 
-	if request != nil {
-		g.requests = append(g.requests, request)
-	}
+	g.requests = append(g.requests, request)
 
 	select {
-	case w.work <- workItem{
+	case w.work <- workItem[T]{
 		process: process,
 		request: request,
 		wg:      g.wg,
@@ -119,7 +117,7 @@ func (g *AsyncGroup) RunAsync(w *Pool, process ProcessFunc, request interface{})
 // CanShareWork determines if the provided worker pool has capacity to share work.
 // If the function returns true, the use MUST call RunAsync() exactly once. This pattern avoids
 // allocations required to create asynchronous input if the worker pool is full.
-func (g *AsyncGroup) CanShareWork(w *Pool) bool {
+func (g *AsyncGroup[T]) CanShareWork(w *Pool[T]) bool {
 	// check pool is not closed
 	select {
 	case <-w.closed:
diff --git a/repo/blob/azure/azure_storage.go b/repo/blob/azure/azure_storage.go
index b6e7b295..0979a486 100644
--- a/repo/blob/azure/azure_storage.go
+++ b/repo/blob/azure/azure_storage.go
@@ -226,7 +226,7 @@ func (az *azStorage) FlushCaches(ctx context.Context) error {
 // New creates new Azure Blob Storage-backed storage with specified options:
 //
 // - the 'Container', 'StorageAccount' and 'StorageKey' fields are required and all other parameters are optional.
-func New(ctx context.Context, opt *Options) (blob.Storage, error) {
+func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) {
 	if opt.Container == "" {
 		return nil, errors.New("container name must be specified")
 	}
@@ -290,12 +290,5 @@ func New(ctx context.Context, opt *Options) (blob.Storage, error) {
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		azStorageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options)) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(azStorageType, Options{}, New)
 }
diff --git a/repo/blob/b2/b2_storage.go b/repo/blob/b2/b2_storage.go
index 7deced8c..2b83956d 100644
--- a/repo/blob/b2/b2_storage.go
+++ b/repo/blob/b2/b2_storage.go
@@ -259,7 +259,7 @@ func (s *b2Storage) String() string {
 }
 
 // New creates new B2-backed storage with specified options.
-func New(ctx context.Context, opt *Options) (blob.Storage, error) {
+func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) {
 	if opt.BucketName == "" {
 		return nil, errors.New("bucket name must be specified")
 	}
@@ -286,12 +286,5 @@ func New(ctx context.Context, opt *Options) (blob.Storage, error) {
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		b2storageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options)) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(b2storageType, Options{}, New)
 }
diff --git a/repo/blob/filesystem/filesystem_storage.go b/repo/blob/filesystem/filesystem_storage.go
index e763f925..9340a313 100644
--- a/repo/blob/filesystem/filesystem_storage.go
+++ b/repo/blob/filesystem/filesystem_storage.go
@@ -117,7 +117,6 @@ func (fs *fsImpl) GetBlobFromPath(ctx context.Context, dirPath, path string, off
 			return blob.ErrBlobNotFound
 		}
 
-		//nolint:wrapcheck
 		return err
 	}
 
@@ -126,7 +125,8 @@ func (fs *fsImpl) GetBlobFromPath(ctx context.Context, dirPath, path string, off
 }
 
 func (fs *fsImpl) GetMetadataFromPath(ctx context.Context, dirPath, path string) (blob.Metadata, error) {
-	v, err := retry.WithExponentialBackoff(ctx, "GetMetadataFromPath:"+path, func() (interface{}, error) {
+	//nolint:wrapcheck
+	return retry.WithExponentialBackoff(ctx, "GetMetadataFromPath:"+path, func() (blob.Metadata, error) {
 		fi, err := fs.osi.Stat(path)
 		if err != nil {
 			if fs.osi.IsNotExist(err) {
@@ -142,12 +142,6 @@ func (fs *fsImpl) GetMetadataFromPath(ctx context.Context, dirPath, path string)
 			Timestamp: fi.ModTime(),
 		}, nil
 	}, fs.isRetriable)
-	if err != nil {
-		//nolint:wrapcheck
-		return blob.Metadata{}, err
-	}
-
-	return v.(blob.Metadata), nil //nolint:forcetypeassert
 }
 
 //nolint:wrapcheck,gocyclo
@@ -248,18 +242,15 @@ func (fs *fsImpl) DeleteBlobInPath(ctx context.Context, dirPath, path string) er
 }
 
 func (fs *fsImpl) ReadDir(ctx context.Context, dirname string) ([]os.FileInfo, error) {
-	v, err := retry.WithExponentialBackoff(ctx, "ReadDir:"+dirname, func() (interface{}, error) {
+	entries, err := retry.WithExponentialBackoff(ctx, "ReadDir:"+dirname, func() ([]os.DirEntry, error) {
 		v, err := fs.osi.ReadDir(dirname)
 		//nolint:wrapcheck
 		return v, err
 	}, fs.isRetriable)
 	if err != nil {
-		//nolint:wrapcheck
 		return nil, err
 	}
 
-	entries := v.([]os.DirEntry) //nolint:forcetypeassert
-
 	fileInfos := make([]os.FileInfo, 0, len(entries))
 
 	for _, e := range entries {
@@ -358,10 +349,5 @@ func New(ctx context.Context, opts *Options, isCreate bool) (blob.Storage, error
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		fsStorageType,
-		func() interface{} { return &Options{} },
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options), isCreate) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(fsStorageType, Options{}, New)
 }
diff --git a/repo/blob/filesystem/filesystem_storage_capacity_openbsd.go b/repo/blob/filesystem/filesystem_storage_capacity_openbsd.go
index 8fc1518b..8eaf31ae 100644
--- a/repo/blob/filesystem/filesystem_storage_capacity_openbsd.go
+++ b/repo/blob/filesystem/filesystem_storage_capacity_openbsd.go
@@ -14,7 +14,7 @@
 )
 
 func (fs *fsStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
-	c, err := retry.WithExponentialBackoff(ctx, "GetCapacity", func() (interface{}, error) {
+	return retry.WithExponentialBackoff(ctx, "GetCapacity", func() (blob.Capacity, error) {
 		var stat syscall.Statfs_t
 		if err := syscall.Statfs(fs.RootPath, &stat); err != nil {
 			return blob.Capacity{}, errors.Wrap(err, "GetCapacity")
@@ -24,7 +24,5 @@ func (fs *fsStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
 			SizeB: uint64(stat.F_blocks) * uint64(stat.F_bsize), //nolint:unconvert,nolintlint
 			FreeB: uint64(stat.F_bavail) * uint64(stat.F_bsize), //nolint:unconvert,nolintlint
 		}, nil
-	}, fs.Impl.(*fsImpl).isRetriable)
-
-	return c.(blob.Capacity), err //nolint:forcetypeassert,wrapcheck
+	}, fs.Impl.(*fsImpl).isRetriable) //nolint:forcetypeassert
 }
diff --git a/repo/blob/filesystem/filesystem_storage_capacity_unix.go b/repo/blob/filesystem/filesystem_storage_capacity_unix.go
index 51194181..7529db44 100644
--- a/repo/blob/filesystem/filesystem_storage_capacity_unix.go
+++ b/repo/blob/filesystem/filesystem_storage_capacity_unix.go
@@ -14,7 +14,7 @@
 )
 
 func (fs *fsStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
-	c, err := retry.WithExponentialBackoff(ctx, "GetCapacity", func() (interface{}, error) {
+	return retry.WithExponentialBackoff(ctx, "GetCapacity", func() (blob.Capacity, error) {
 		var stat syscall.Statfs_t
 		if err := syscall.Statfs(fs.RootPath, &stat); err != nil {
 			return blob.Capacity{}, errors.Wrap(err, "GetCapacity")
@@ -24,7 +24,5 @@ func (fs *fsStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
 			SizeB: uint64(stat.Blocks) * uint64(stat.Bsize), //nolint:unconvert
 			FreeB: uint64(stat.Bavail) * uint64(stat.Bsize), //nolint:unconvert
 		}, nil
-	}, fs.Impl.(*fsImpl).isRetriable)
-
-	return c.(blob.Capacity), err //nolint:forcetypeassert,wrapcheck
+	}, fs.Impl.(*fsImpl).isRetriable) //nolint:forcetypeassert
 }
diff --git a/repo/blob/gcs/gcs_storage.go b/repo/blob/gcs/gcs_storage.go
index 4d60ac16..8fa8bd4b 100644
--- a/repo/blob/gcs/gcs_storage.go
+++ b/repo/blob/gcs/gcs_storage.go
@@ -241,7 +241,7 @@ func tokenSourceFromCredentialsJSON(ctx context.Context, data json.RawMessage, s
 //
 // By default the connection reuses credentials managed by (https://cloud.google.com/sdk/),
 // but this can be disabled by setting IgnoreDefaultCredentials to true.
-func New(ctx context.Context, opt *Options) (blob.Storage, error) {
+func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) {
 	var ts oauth2.TokenSource
 
 	var err error
@@ -295,12 +295,5 @@ func New(ctx context.Context, opt *Options) (blob.Storage, error) {
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		gcsStorageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options)) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(gcsStorageType, Options{}, New)
 }
diff --git a/repo/blob/gdrive/gdrive_storage.go b/repo/blob/gdrive/gdrive_storage.go
index 15ca3b1e..9501cf51 100644
--- a/repo/blob/gdrive/gdrive_storage.go
+++ b/repo/blob/gdrive/gdrive_storage.go
@@ -362,14 +362,9 @@ func (gdrive *gdriveStorage) getFileIDWithCache(ctx context.Context, entry *cach
 
 // Try getFileByBlobID with periodic backoff.
 func (gdrive *gdriveStorage) tryGetFileByBlobID(ctx context.Context, blobID blob.ID, fields googleapi.Field) (*drive.File, error) {
-	f, err := retry.Periodically(ctx, queryRetryDelay, queryRetryMax, fmt.Sprintf("getFileIDByblobID(%v)", blobID), func() (interface{}, error) {
+	return retry.Periodically(ctx, queryRetryDelay, queryRetryMax, fmt.Sprintf("getFileIDByblobID(%v)", blobID), func() (*drive.File, error) {
 		return gdrive.getFileByBlobID(ctx, blobID, fields)
 	}, retryNotFound)
-	if err != nil {
-		return nil, err //nolint:wrapcheck
-	}
-
-	return f.(*drive.File), nil //nolint:forcetypeassert
 }
 
 func (gdrive *gdriveStorage) getFileByFileID(ctx context.Context, fileID string, fields googleapi.Field) (*drive.File, error) {
@@ -545,7 +540,7 @@ func CreateDriveService(ctx context.Context, opt *Options) (*drive.Service, erro
 //
 // By default the connection reuses credentials managed by (https://cloud.google.com/sdk/),
 // but this can be disabled by setting IgnoreDefaultCredentials to true.
-func New(ctx context.Context, opt *Options) (blob.Storage, error) {
+func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) {
 	if opt.FolderID == "" {
 		return nil, errors.New("folder-id must be specified")
 	}
@@ -578,12 +573,5 @@ func New(ctx context.Context, opt *Options) (blob.Storage, error) {
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		gdriveStorageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options)) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(gdriveStorageType, Options{}, New)
 }
diff --git a/repo/blob/rclone/rclone_storage.go b/repo/blob/rclone/rclone_storage.go
index 218fb785..c2ea5d8e 100644
--- a/repo/blob/rclone/rclone_storage.go
+++ b/repo/blob/rclone/rclone_storage.go
@@ -324,12 +324,5 @@ func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error)
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		rcloneStorageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options), isCreate) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(rcloneStorageType, Options{}, New)
 }
diff --git a/repo/blob/registry.go b/repo/blob/registry.go
index c1558681..0fabce82 100644
--- a/repo/blob/registry.go
+++ b/repo/blob/registry.go
@@ -6,28 +6,30 @@
 	"github.com/pkg/errors"
 )
 
-// CreateStorageFunc is a function that returns Storage with provided options, optionally
-// creating the underlying storage location (e.g. directory), when possible.
-type CreateStorageFunc func(ctx context.Context, options interface{}, isCreate bool) (Storage, error)
-
 //nolint:gochecknoglobals
 var factories = map[string]*storageFactory{}
 
 // StorageFactory allows creation of repositories in a generic way.
 type storageFactory struct {
 	defaultConfigFunc func() interface{}
-	createStorageFunc CreateStorageFunc
+	createStorageFunc func(ctx context.Context, options interface{}, isCreate bool) (Storage, error)
 }
 
 // AddSupportedStorage registers factory function to create storage with a given type name.
-func AddSupportedStorage(
+func AddSupportedStorage[T any](
 	urlScheme string,
-	defaultConfigFunc func() interface{},
-	createStorageFunc CreateStorageFunc,
+	defaultConfig T,
+	createStorageFunc func(ctx context.Context, options *T, isCreate bool) (Storage, error),
 ) {
 	f := &storageFactory{
-		defaultConfigFunc: defaultConfigFunc,
-		createStorageFunc: createStorageFunc,
+		defaultConfigFunc: func() interface{} {
+			c := defaultConfig
+			return &c
+		},
+		createStorageFunc: func(ctx context.Context, options interface{}, isCreate bool) (Storage, error) {
+			//nolint:forcetypeassert
+			return createStorageFunc(ctx, options.(*T), isCreate)
+		},
 	}
 
 	factories[urlScheme] = f
diff --git a/repo/blob/retrying/retrying_storage.go b/repo/blob/retrying/retrying_storage.go
index ce37c57c..9c158703 100644
--- a/repo/blob/retrying/retrying_storage.go
+++ b/repo/blob/retrying/retrying_storage.go
@@ -27,33 +27,24 @@ func (s retryingStorage) GetBlob(ctx context.Context, id blob.ID, offset, length
 }
 
 func (s retryingStorage) GetMetadata(ctx context.Context, id blob.ID) (blob.Metadata, error) {
-	v, err := retry.WithExponentialBackoff(ctx, "GetMetadata("+string(id)+")", func() (interface{}, error) {
+	return retry.WithExponentialBackoff(ctx, "GetMetadata("+string(id)+")", func() (blob.Metadata, error) {
 		//nolint:wrapcheck
 		return s.Storage.GetMetadata(ctx, id)
 	}, isRetriable)
-	if err != nil {
-		return blob.Metadata{}, err //nolint:wrapcheck
-	}
-
-	return v.(blob.Metadata), nil //nolint:forcetypeassert
 }
 
 func (s retryingStorage) PutBlob(ctx context.Context, id blob.ID, data blob.Bytes, opts blob.PutOptions) error {
-	_, err := retry.WithExponentialBackoff(ctx, "PutBlob("+string(id)+")", func() (interface{}, error) {
+	return retry.WithExponentialBackoffNoValue(ctx, "PutBlob("+string(id)+")", func() error {
 		//nolint:wrapcheck
-		return true, s.Storage.PutBlob(ctx, id, data, opts)
+		return s.Storage.PutBlob(ctx, id, data, opts)
 	}, isRetriable)
-
-	return err //nolint:wrapcheck
 }
 
 func (s retryingStorage) DeleteBlob(ctx context.Context, id blob.ID) error {
-	_, err := retry.WithExponentialBackoff(ctx, "DeleteBlob("+string(id)+")", func() (interface{}, error) {
-		//nolint:wrapcheck
-		return true, s.Storage.DeleteBlob(ctx, id)
+	//nolint:wrapcheck
+	return retry.WithExponentialBackoffNoValue(ctx, "DeleteBlob("+string(id)+")", func() error {
+		return s.Storage.DeleteBlob(ctx, id)
 	}, isRetriable)
-
-	return err //nolint:wrapcheck
 }
 
 // NewWrapper returns a Storage wrapper that adds retry loop around all operations of the underlying storage.
diff --git a/repo/blob/s3/s3_storage.go b/repo/blob/s3/s3_storage.go
index af56a23b..0f6f8b79 100644
--- a/repo/blob/s3/s3_storage.go
+++ b/repo/blob/s3/s3_storage.go
@@ -301,7 +301,7 @@ func getCustomTransport(insecureSkipVerify bool) (transport *http.Transport) {
 // New creates new S3-backed storage with specified options:
 //
 // - the 'BucketName' field is required and all other parameters are optional.
-func New(ctx context.Context, opt *Options) (blob.Storage, error) {
+func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error) {
 	st, err := newStorage(ctx, opt)
 	if err != nil {
 		return nil, err
@@ -387,12 +387,5 @@ func newStorageWithCredentials(ctx context.Context, creds *credentials.Credentia
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		s3storageType,
-		func() interface{} {
-			return &Options{}
-		},
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options)) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(s3storageType, Options{}, New)
 }
diff --git a/repo/blob/sftp/sftp_storage.go b/repo/blob/sftp/sftp_storage.go
index ec8f4098..fcce7718 100644
--- a/repo/blob/sftp/sftp_storage.go
+++ b/repo/blob/sftp/sftp_storage.go
@@ -102,7 +102,8 @@ func (s *sftpImpl) IsConnectionClosedError(err error) bool {
 }
 
 func (s *sftpStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
-	i, err := s.Impl.(*sftpImpl).rec.UsingConnection(ctx, "GetCapacity", func(conn connection.Connection) (interface{}, error) {
+	//nolint:forcetypeassert
+	return connection.UsingConnection(ctx, s.Impl.(*sftpImpl).rec, "GetCapacity", func(conn connection.Connection) (blob.Capacity, error) {
 		stat, err := sftpClientFromConnection(conn).StatVFS(s.RootPath)
 		if err != nil {
 			return blob.Capacity{}, errors.Wrap(err, "GetCapacity")
@@ -113,8 +114,6 @@ func (s *sftpStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
 			FreeB: stat.Bfree * stat.Bsize,
 		}, err //nolint:wrapcheck
 	})
-
-	return i.(blob.Capacity), err //nolint:forcetypeassert,wrapcheck
 }
 
 func (s *sftpImpl) GetBlobFromPath(ctx context.Context, dirPath, fullPath string, offset, length int64, output blob.OutputBuffer) error {
@@ -163,7 +162,7 @@ func (s *sftpImpl) GetBlobFromPath(ctx context.Context, dirPath, fullPath string
 }
 
 func (s *sftpImpl) GetMetadataFromPath(ctx context.Context, dirPath, fullPath string) (blob.Metadata, error) {
-	v, err := s.rec.UsingConnection(ctx, "GetMetadataFromPath", func(conn connection.Connection) (interface{}, error) {
+	return connection.UsingConnection(ctx, s.rec, "GetMetadataFromPath", func(conn connection.Connection) (blob.Metadata, error) {
 		fi, err := sftpClientFromConnection(conn).Stat(fullPath)
 		if isNotExist(err) {
 			return blob.Metadata{}, blob.ErrBlobNotFound
@@ -178,12 +177,6 @@ func (s *sftpImpl) GetMetadataFromPath(ctx context.Context, dirPath, fullPath st
 			Timestamp: fi.ModTime(),
 		}, nil
 	})
-	if err != nil {
-		//nolint:wrapcheck
-		return blob.Metadata{}, err
-	}
-
-	return v.(blob.Metadata), nil //nolint:forcetypeassert
 }
 
 func (s *sftpImpl) PutBlobInPath(ctx context.Context, dirPath, fullPath string, data blob.Bytes, opts blob.PutOptions) error {
@@ -307,16 +300,10 @@ func (s *sftpImpl) DeleteBlobInPath(ctx context.Context, dirPath, fullPath strin
 }
 
 func (s *sftpImpl) ReadDir(ctx context.Context, dirname string) ([]os.FileInfo, error) {
-	v, err := s.rec.UsingConnection(ctx, "ReadDir", func(conn connection.Connection) (interface{}, error) {
+	return connection.UsingConnection(ctx, s.rec, "ReadDir", func(conn connection.Connection) ([]os.FileInfo, error) {
 		//nolint:wrapcheck
 		return sftpClientFromConnection(conn).ReadDir(dirname)
 	})
-	if err != nil {
-		//nolint:wrapcheck
-		return nil, err
-	}
-
-	return v.([]os.FileInfo), nil //nolint:forcetypeassert
 }
 
 func (s *sftpStorage) ConnectionInfo() blob.ConnectionInfo {
@@ -571,10 +558,5 @@ func sftpClientFromConnection(conn connection.Connection) *sftp.Client {
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		sftpStorageType,
-		func() interface{} { return &Options{} },
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options), isCreate) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(sftpStorageType, Options{}, New)
 }
diff --git a/repo/blob/webdav/webdav_storage.go b/repo/blob/webdav/webdav_storage.go
index ea1d9278..9e17d73a 100644
--- a/repo/blob/webdav/webdav_storage.go
+++ b/repo/blob/webdav/webdav_storage.go
@@ -276,10 +276,5 @@ func New(ctx context.Context, opts *Options, isCreate bool) (blob.Storage, error
 }
 
 func init() {
-	blob.AddSupportedStorage(
-		davStorageType,
-		func() interface{} { return &Options{} },
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			return New(ctx, o.(*Options), isCreate) //nolint:forcetypeassert
-		})
+	blob.AddSupportedStorage(davStorageType, Options{}, New)
 }
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 18d662b7..f5f8a7a6 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -247,14 +247,9 @@ func (r *grpcInnerSession) initializeSession(ctx context.Context, purpose string
 }
 
 func (r *grpcRepositoryClient) GetManifest(ctx context.Context, id manifest.ID, data interface{}) (*manifest.EntryMetadata, error) {
-	v, err := r.maybeRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	return maybeRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (*manifest.EntryMetadata, error) {
 		return sess.GetManifest(ctx, id, data)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return v.(*manifest.EntryMetadata), nil //nolint:forcetypeassert
 }
 
 func (r *grpcInnerSession) GetManifest(ctx context.Context, id manifest.ID, data interface{}) (*manifest.EntryMetadata, error) {
@@ -297,14 +292,9 @@ func decodeManifestEntryMetadata(md *apipb.ManifestEntryMetadata) *manifest.Entr
 }
 
 func (r *grpcRepositoryClient) PutManifest(ctx context.Context, labels map[string]string, payload interface{}) (manifest.ID, error) {
-	v, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	return inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (manifest.ID, error) {
 		return sess.PutManifest(ctx, labels, payload)
 	})
-	if err != nil {
-		return "", err
-	}
-
-	return v.(manifest.ID), nil //nolint:forcetypeassert
 }
 
 func (r *grpcInnerSession) PutManifest(ctx context.Context, labels map[string]string, payload interface{}) (manifest.ID, error) {
@@ -334,14 +324,9 @@ func (r *grpcInnerSession) PutManifest(ctx context.Context, labels map[string]st
 }
 
 func (r *grpcRepositoryClient) FindManifests(ctx context.Context, labels map[string]string) ([]*manifest.EntryMetadata, error) {
-	v, err := r.maybeRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	return maybeRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) ([]*manifest.EntryMetadata, error) {
 		return sess.FindManifests(ctx, labels)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return v.([]*manifest.EntryMetadata), nil //nolint:forcetypeassert
 }
 
 func (r *grpcInnerSession) FindManifests(ctx context.Context, labels map[string]string) ([]*manifest.EntryMetadata, error) {
@@ -365,7 +350,7 @@ func (r *grpcInnerSession) FindManifests(ctx context.Context, labels map[string]
 }
 
 func (r *grpcRepositoryClient) DeleteManifest(ctx context.Context, id manifest.ID) error {
-	_, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	_, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
 		return false, sess.DeleteManifest(ctx, id)
 	})
 
@@ -398,15 +383,11 @@ func (r *grpcRepositoryClient) PrefetchObjects(ctx context.Context, objectIDs []
 }
 
 func (r *grpcRepositoryClient) PrefetchContents(ctx context.Context, contentIDs []content.ID, hint string) []content.ID {
-	ids, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	result, _ := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) ([]content.ID, error) {
 		return sess.PrefetchContents(ctx, contentIDs, hint), nil
 	})
-	if err != nil {
-		return nil
-	}
 
-	//nolint:forcetypeassert
-	return ids.([]content.ID)
+	return result
 }
 
 func (r *grpcInnerSession) PrefetchContents(ctx context.Context, contentIDs []content.ID, hint string) []content.ID {
@@ -451,7 +432,7 @@ func (r *grpcRepositoryClient) Flush(ctx context.Context) error {
 		return errors.Wrap(err, "error waiting for async writes")
 	}
 
-	_, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	_, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
 		return false, sess.Flush(ctx)
 	})
 
@@ -491,29 +472,28 @@ func (r *grpcRepositoryClient) ConcatenateObjects(ctx context.Context, objectIDs
 	return r.omgr.Concatenate(ctx, objectIDs)
 }
 
-type sessionAttemptFunc func(ctx context.Context, sess *grpcInnerSession) (interface{}, error)
-
 // maybeRetry executes the provided callback with or without automatic retries depending on how
 // the grpcRepositoryClient is configured.
-func (r *grpcRepositoryClient) maybeRetry(ctx context.Context, attempt sessionAttemptFunc) (interface{}, error) {
+func maybeRetry[T any](ctx context.Context, r *grpcRepositoryClient, attempt func(ctx context.Context, sess *grpcInnerSession) (T, error)) (T, error) {
 	if !r.transparentRetries {
-		return r.inSessionWithoutRetry(ctx, attempt)
+		return inSessionWithoutRetry(ctx, r, attempt)
 	}
 
-	return r.retry(ctx, attempt)
+	return doRetry(ctx, r, attempt)
 }
 
 // retry executes the provided callback and provides it with *grpcInnerSession.
 // If the grpcRepositoryClient set to automatically retry and the provided callback returns io.EOF,
 // the inner session will be killed and re-established as necessary.
-func (r *grpcRepositoryClient) retry(ctx context.Context, attempt sessionAttemptFunc) (interface{}, error) {
-	//nolint:wrapcheck
-	return retry.WithExponentialBackoff(ctx, "invoking GRPC API", func() (interface{}, error) {
-		v, err := r.inSessionWithoutRetry(ctx, attempt)
+func doRetry[T any](ctx context.Context, r *grpcRepositoryClient, attempt func(ctx context.Context, sess *grpcInnerSession) (T, error)) (T, error) {
+	var defaultT T
+
+	return retry.WithExponentialBackoff(ctx, "invoking GRPC API", func() (T, error) {
+		v, err := inSessionWithoutRetry(ctx, r, attempt)
 		if errors.Is(err, io.EOF) {
 			r.killInnerSession()
 
-			return nil, errShouldRetry
+			return defaultT, errShouldRetry
 		}
 
 		return v, err
@@ -522,24 +502,21 @@ func (r *grpcRepositoryClient) retry(ctx context.Context, attempt sessionAttempt
 	})
 }
 
-func (r *grpcRepositoryClient) inSessionWithoutRetry(ctx context.Context, attempt sessionAttemptFunc) (interface{}, error) {
+func inSessionWithoutRetry[T any](ctx context.Context, r *grpcRepositoryClient, attempt func(ctx context.Context, sess *grpcInnerSession) (T, error)) (T, error) {
+	var defaultT T
+
 	sess, err := r.getOrEstablishInnerSession(ctx)
 	if err != nil {
-		return nil, errors.Wrapf(err, "unable to establish session for purpose=%v", r.opt.Purpose)
+		return defaultT, errors.Wrapf(err, "unable to establish session for purpose=%v", r.opt.Purpose)
 	}
 
 	return attempt(ctx, sess)
 }
 
 func (r *grpcRepositoryClient) ContentInfo(ctx context.Context, contentID content.ID) (content.Info, error) {
-	v, err := r.maybeRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	return maybeRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (content.Info, error) {
 		return sess.contentInfo(ctx, contentID)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return v.(content.Info), nil //nolint:forcetypeassert
 }
 
 func (r *grpcInnerSession) contentInfo(ctx context.Context, contentID content.ID) (content.Info, error) {
@@ -604,14 +581,14 @@ func (r *grpcRepositoryClient) GetContent(ctx context.Context, contentID content
 	defer b.Close()
 
 	err := r.contentCache.GetOrLoad(ctx, contentID.String(), func(output *gather.WriteBuffer) error {
-		v, err := r.maybeRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+		v, err := maybeRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) ([]byte, error) {
 			return sess.GetContent(ctx, contentID)
 		})
 		if err != nil {
 			return err
 		}
 
-		_, err = output.Write(v.([]byte))
+		_, err = output.Write(v)
 
 		//nolint:wrapcheck
 		return err
@@ -661,10 +638,9 @@ func (r *grpcRepositoryClient) doWriteAsync(ctx context.Context, contentID conte
 
 	r.opt.OnUpload(int64(len(data)))
 
-	if _, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	if _, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (content.ID, error) {
 		sess.WriteContentAsyncAndVerify(ctx, contentID, data, prefix, comp, r.asyncWritesWG)
-
-		return nil, nil
+		return contentID, nil
 	}); err != nil {
 		return err
 	}
@@ -851,7 +827,7 @@ func (r *grpcRepositoryClient) getOrEstablishInnerSession(ctx context.Context) (
 
 		r.innerSessionAttemptCount++
 
-		v, err := retry.WithExponentialBackoff(ctx, "establishing session", func() (interface{}, error) {
+		v, err := retry.WithExponentialBackoff(ctx, "establishing session", func() (*grpcInnerSession, error) {
 			sess, err := cli.Session(ctxutil.Detach(ctx))
 			if err != nil {
 				return nil, errors.Wrap(err, "Session()")
@@ -876,8 +852,7 @@ func (r *grpcRepositoryClient) getOrEstablishInnerSession(ctx context.Context) (
 			return nil, errors.Wrap(err, "error establishing session")
 		}
 
-		//nolint:forcetypeassert
-		r.innerSession = v.(*grpcInnerSession)
+		r.innerSession = v
 	}
 
 	return r.innerSession, nil
@@ -910,7 +885,7 @@ func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientCon
 		asyncWritesWG:      new(errgroup.Group),
 	}
 
-	v, err := rr.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) {
+	return inSessionWithoutRetry(ctx, rr, func(ctx context.Context, sess *grpcInnerSession) (*grpcRepositoryClient, error) {
 		p := sess.repoParams
 		hf, err := hashing.CreateHashFunc(p)
 		if err != nil {
@@ -934,9 +909,4 @@ func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientCon
 
 		return rr, nil
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return v.(*grpcRepositoryClient), nil //nolint:forcetypeassert
 }
diff --git a/repo/open.go b/repo/open.go
index 5b77b2ad..f299e1ef 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -228,11 +228,10 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 			return nil, ErrRepositoryUnavailableDueToUpgrageInProgress
 		}
 
-		return nil, nil
+		return false, nil
 	}, func(internalErr error) bool {
 		return !options.DoNotWaitForUpgrade && errors.Is(internalErr, ErrRepositoryUnavailableDueToUpgrageInProgress)
 	}); err != nil {
-		//nolint:wrapcheck
 		return nil, err
 	}
 
diff --git a/snapshot/snapshotfs/dir_rewriter.go b/snapshot/snapshotfs/dir_rewriter.go
index 5dbb6deb..43ce7463 100644
--- a/snapshot/snapshotfs/dir_rewriter.go
+++ b/snapshot/snapshotfs/dir_rewriter.go
@@ -50,7 +50,7 @@ type DirRewriterOptions struct {
 
 // DirRewriter rewrites contents of directories by walking the snapshot tree recursively.
 type DirRewriter struct {
-	ws   *workshare.Pool
+	ws   *workshare.Pool[*dirRewriterRequest]
 	opts DirRewriterOptions
 
 	cache *bigmap.Map
@@ -66,9 +66,7 @@ type dirRewriterRequest struct {
 	err        error
 }
 
-func (rw *DirRewriter) processRequest(pool *workshare.Pool, req0 interface{}) {
-	req, _ := req0.(*dirRewriterRequest)
-
+func (rw *DirRewriter) processRequest(pool *workshare.Pool[*dirRewriterRequest], req *dirRewriterRequest) {
 	req.result, req.err = rw.getCachedReplacement(req.ctx, req.parentPath, req.input)
 }
 
@@ -151,7 +149,7 @@ func (rw *DirRewriter) processDirectory(ctx context.Context, pathFromRoot string
 func (rw *DirRewriter) processDirectoryEntries(ctx context.Context, parentPath string, entry *snapshot.DirEntry, entries []*snapshot.DirEntry) (*snapshot.DirEntry, error) {
 	var (
 		builder DirManifestBuilder
-		wg      workshare.AsyncGroup
+		wg      workshare.AsyncGroup[*dirRewriterRequest]
 	)
 
 	// ensure we wait for all work items before returning
@@ -187,10 +185,8 @@ func (rw *DirRewriter) processDirectoryEntries(ctx context.Context, parentPath s
 	// now wait for all asynchronous work to complete and add resulting entries to
 	// the builder
 	for _, req := range wg.Wait() {
-		if req, ok := req.(*dirRewriterRequest); ok {
-			if req.result != nil {
-				builder.AddEntry(req.result)
-			}
+		if req.result != nil {
+			builder.AddEntry(req.result)
 		}
 	}
 
@@ -322,7 +318,7 @@ func NewDirRewriter(ctx context.Context, rep repo.RepositoryWriter, opts DirRewr
 	}
 
 	return &DirRewriter{
-		ws:    workshare.NewPool(opts.Parallel - 1),
+		ws:    workshare.NewPool[*dirRewriterRequest](opts.Parallel - 1),
 		opts:  opts,
 		rep:   rep,
 		cache: cache,
diff --git a/snapshot/snapshotfs/snapshot_tree_walker.go b/snapshot/snapshotfs/snapshot_tree_walker.go
index 94a80d4e..2ed30160 100644
--- a/snapshot/snapshotfs/snapshot_tree_walker.go
+++ b/snapshot/snapshotfs/snapshot_tree_walker.go
@@ -25,7 +25,7 @@ type TreeWalker struct {
 	options TreeWalkerOptions
 
 	enqueued *bigmap.Set
-	wp       *workshare.Pool
+	wp       *workshare.Pool[any]
 
 	mu sync.Mutex
 	// +checklocks:mu
@@ -111,7 +111,7 @@ type errStop struct {
 		error
 	}
 
-	var ag workshare.AsyncGroup
+	var ag workshare.AsyncGroup[any]
 	defer ag.Close()
 
 	err := dir.IterateEntries(ctx, func(c context.Context, ent fs.Entry) error {
@@ -126,7 +126,7 @@ type errStop struct {
 		childPath := path.Join(entryPath, ent.Name())
 
 		if ag.CanShareWork(w.wp) {
-			ag.RunAsync(w.wp, func(c *workshare.Pool, request interface{}) {
+			ag.RunAsync(w.wp, func(c *workshare.Pool[any], request any) {
 				w.processEntry(ctx, ent, childPath)
 			}, nil)
 		} else {
@@ -188,7 +188,7 @@ func NewTreeWalker(ctx context.Context, options TreeWalkerOptions) (*TreeWalker,
 
 	return &TreeWalker{
 		options:  options,
-		wp:       workshare.NewPool(options.Parallelism - 1),
+		wp:       workshare.NewPool[any](options.Parallelism - 1),
 		enqueued: s,
 	}, nil
 }
diff --git a/snapshot/snapshotfs/upload.go b/snapshot/snapshotfs/upload.go
index 1ae6f50b..f100eb74 100644
--- a/snapshot/snapshotfs/upload.go
+++ b/snapshot/snapshotfs/upload.go
@@ -113,7 +113,7 @@ type Uploader struct {
 	// disable snapshot size estimation
 	disableEstimation bool
 
-	workerPool *workshare.Pool
+	workerPool *workshare.Pool[*uploadWorkItem]
 
 	traceEnabled bool
 }
@@ -175,7 +175,7 @@ func (u *Uploader) uploadFileInternal(ctx context.Context, parentCheckpointRegis
 	parts := make([]*snapshot.DirEntry, fullParts+1)
 	partErrors := make([]error, fullParts+1)
 
-	var wg workshare.AsyncGroup
+	var wg workshare.AsyncGroup[*uploadWorkItem]
 	defer wg.Close()
 
 	for i := 0; i < len(parts); i++ {
@@ -190,7 +190,7 @@ func (u *Uploader) uploadFileInternal(ctx context.Context, parentCheckpointRegis
 
 		if wg.CanShareWork(u.workerPool) {
 			// another goroutine is available, delegate to them
-			wg.RunAsync(u.workerPool, func(c *workshare.Pool, request interface{}) {
+			wg.RunAsync(u.workerPool, func(c *workshare.Pool[*uploadWorkItem], request *uploadWorkItem) {
 				parts[i], partErrors[i] = u.uploadFileData(ctx, parentCheckpointRegistry, f, uuid.NewString(), offset, length, comp)
 			}, nil)
 		} else {
@@ -620,7 +620,7 @@ func (u *Uploader) processChildren(
 	policyTree *policy.Tree,
 	previousDirs []fs.Directory,
 ) error {
-	var wg workshare.AsyncGroup
+	var wg workshare.AsyncGroup[*uploadWorkItem]
 
 	// ensure we wait for all work items before returning
 	defer wg.Close()
@@ -633,12 +633,7 @@ func (u *Uploader) processChildren(
 	}
 
 	for _, wi := range wg.Wait() {
-		wi, ok := wi.(*uploadWorkItem)
-		if !ok {
-			return errors.Errorf("unexpected work item type %T", wi)
-		}
-
-		if wi.err != nil {
+		if wi != nil && wi.err != nil {
 			return wi.err
 		}
 	}
@@ -750,7 +745,7 @@ func (u *Uploader) processDirectoryEntries(
 	dir fs.Directory,
 	policyTree *policy.Tree,
 	prevDirs []fs.Directory,
-	wg *workshare.AsyncGroup,
+	wg *workshare.AsyncGroup[*uploadWorkItem],
 ) error {
 	// processEntryError distinguishes an error thrown when attempting to read a directory.
 	type processEntryError struct {
@@ -765,8 +760,7 @@ type processEntryError struct {
 		entryRelativePath := path.Join(dirRelativePath, entry.Name())
 
 		if wg.CanShareWork(u.workerPool) {
-			wg.RunAsync(u.workerPool, func(c *workshare.Pool, input interface{}) {
-				wi, _ := input.(*uploadWorkItem)
+			wg.RunAsync(u.workerPool, func(c *workshare.Pool[*uploadWorkItem], wi *uploadWorkItem) {
 				wi.err = u.processSingle(ctx, entry, entryRelativePath, parentDirBuilder, policyTree, prevDirs, localDirPathOrEmpty, parentCheckpointRegistry)
 			}, &uploadWorkItem{})
 		} else {
@@ -1225,7 +1219,7 @@ func (u *Uploader) Upload(
 		Source: sourceInfo,
 	}
 
-	u.workerPool = workshare.NewPool(parallel - 1)
+	u.workerPool = workshare.NewPool[*uploadWorkItem](parallel - 1)
 	defer u.workerPool.Close()
 
 	u.stats = &snapshot.Stats{}


Test patch:
diff --git a/internal/connection/reconnector_test.go b/internal/connection/reconnector_test.go
index 10427ce5..cf617610 100644
--- a/internal/connection/reconnector_test.go
+++ b/internal/connection/reconnector_test.go
@@ -74,7 +74,7 @@ func TestConnection(t *testing.T) {
 
 	r := connection.NewReconnector(fc)
 
-	v, err := r.UsingConnection(ctx, "first", func(cli connection.Connection) (interface{}, error) {
+	v, err := connection.UsingConnection(ctx, r, "first", func(cli connection.Connection) (interface{}, error) {
 		require.EqualValues(t, 1, cli.(*fakeConnection).id)
 		return "foo", nil
 	})
diff --git a/internal/repotesting/reconnectable_storage.go b/internal/repotesting/reconnectable_storage.go
index c2087c22..1229d15e 100644
--- a/internal/repotesting/reconnectable_storage.go
+++ b/internal/repotesting/reconnectable_storage.go
@@ -56,25 +56,20 @@ func (s reconnectableStorage) ConnectionInfo() blob.ConnectionInfo {
 	}
 }
 
+// New creates new reconnectable storage.
+func New(ctx context.Context, opt *ReconnectableStorageOptions, isCreate bool) (blob.Storage, error) {
+	if opt.UUID == "" {
+		return nil, errors.Errorf("missing UUID")
+	}
+
+	v, ok := reconnectableStorageByUUID.Load(opt.UUID)
+	if !ok {
+		return nil, errors.Errorf("reconnectable storage not found: %v", opt.UUID)
+	}
+
+	return v.(blob.Storage), nil
+}
+
 func init() {
-	blob.AddSupportedStorage(
-		ReconnectableStorageType,
-		func() interface{} { return &ReconnectableStorageOptions{} },
-		func(ctx context.Context, o interface{}, isCreate bool) (blob.Storage, error) {
-			opt, ok := o.(*ReconnectableStorageOptions)
-			if !ok {
-				return nil, errors.Errorf("invalid options %T", o)
-			}
-
-			if opt.UUID == "" {
-				return nil, errors.Errorf("missing UUID")
-			}
-
-			v, ok := reconnectableStorageByUUID.Load(opt.UUID)
-			if !ok {
-				return nil, errors.Errorf("reconnectable storage not found: %v", opt.UUID)
-			}
-
-			return v.(blob.Storage), nil
-		})
+	blob.AddSupportedStorage(ReconnectableStorageType, ReconnectableStorageOptions{}, New)
 }
diff --git a/internal/retry/retry_test.go b/internal/retry/retry_test.go
index 48624f48..089f0c53 100644
--- a/internal/retry/retry_test.go
+++ b/internal/retry/retry_test.go
@@ -26,20 +26,20 @@ func TestRetry(t *testing.T) {
 
 	cases := []struct {
 		desc      string
-		f         func() (interface{}, error)
-		want      interface{}
+		f         func() (int, error)
+		want      int
 		wantError error
 	}{
-		{"success-nil", func() (interface{}, error) { return nil, nil }, nil, nil},
-		{"success", func() (interface{}, error) { return 3, nil }, 3, nil},
-		{"retriable-succeeds", func() (interface{}, error) {
+		{"success-nil", func() (int, error) { return 0, nil }, 0, nil},
+		{"success", func() (int, error) { return 3, nil }, 3, nil},
+		{"retriable-succeeds", func() (int, error) {
 			cnt++
 			if cnt < 2 {
-				return nil, errRetriable
+				return 0, errRetriable
 			}
 			return 4, nil
 		}, 4, nil},
-		{"retriable-never-succeeds", func() (interface{}, error) { return nil, errRetriable }, nil, errors.Errorf("unable to complete retriable-never-succeeds despite 3 retries")},
+		{"retriable-never-succeeds", func() (int, error) { return 0, errRetriable }, 0, errors.Errorf("unable to complete retriable-never-succeeds despite 3 retries")},
 	}
 
 	ctx := testlogging.Context(t)
diff --git a/internal/workshare/workshare_test.go b/internal/workshare/workshare_test.go
index f600f7f1..0d168fb1 100644
--- a/internal/workshare/workshare_test.go
+++ b/internal/workshare/workshare_test.go
@@ -36,9 +36,7 @@ type computeTreeSumRequest struct {
 	err    error
 }
 
-func dispatchComputeTreeSumRequest(w *workshare.Pool, input interface{}) {
-	req := input.(*computeTreeSumRequest)
-
+func dispatchComputeTreeSumRequest(w *workshare.Pool[*computeTreeSumRequest], req *computeTreeSumRequest) {
 	if w.ActiveWorkers() == 0 {
 		panic("unexpected worker count")
 	}
@@ -52,10 +50,10 @@ func dispatchComputeTreeSumRequest(w *workshare.Pool, input interface{}) {
 	req.result = res
 }
 
-func computeTreeSum(workPool *workshare.Pool, n *treeNode) (int, error) {
+func computeTreeSum(workPool *workshare.Pool[*computeTreeSumRequest], n *treeNode) (int, error) {
 	total := n.value
 
-	var cs workshare.AsyncGroup
+	var cs workshare.AsyncGroup[*computeTreeSumRequest]
 
 	for _, child := range n.children {
 		if cs.CanShareWork(workPool) {
@@ -74,7 +72,7 @@ func computeTreeSum(workPool *workshare.Pool, n *treeNode) (int, error) {
 	}
 
 	for _, req := range cs.Wait() {
-		twr := req.(*computeTreeSumRequest)
+		twr := req
 
 		if twr.err != nil {
 			return 0, twr.err
@@ -103,7 +101,7 @@ func TestComputeTreeSumNegative(t *testing.T) {
 }
 
 func TestDisallowed_DoubleWait(t *testing.T) {
-	var ag workshare.AsyncGroup
+	var ag workshare.AsyncGroup[int]
 
 	ag.Wait()
 	require.Panics(t, func() {
@@ -112,7 +110,7 @@ func TestDisallowed_DoubleWait(t *testing.T) {
 }
 
 func TestDisallowed_WaitAfterClose(t *testing.T) {
-	var ag workshare.AsyncGroup
+	var ag workshare.AsyncGroup[int]
 
 	ag.Close()
 	require.Panics(t, func() {
@@ -123,9 +121,9 @@ func TestDisallowed_WaitAfterClose(t *testing.T) {
 }
 
 func TestDisallowed_UseAfterPoolClose(t *testing.T) {
-	w := workshare.NewPool(1)
+	w := workshare.NewPool[int](1)
 
-	var ag workshare.AsyncGroup
+	var ag workshare.AsyncGroup[int]
 
 	w.Close()
 
@@ -134,15 +132,15 @@ func TestDisallowed_UseAfterPoolClose(t *testing.T) {
 	})
 
 	require.Panics(t, func() {
-		ag.RunAsync(w, func(c *workshare.Pool, request interface{}) {
+		ag.RunAsync(w, func(c *workshare.Pool[int], request int) {
 			t.Fatal("should not be called")
-		}, nil)
+		}, 33)
 	})
 }
 
 //nolint:thelper
 func testComputeTreeSum(t *testing.T, numWorkers int) {
-	w := workshare.NewPool(numWorkers)
+	w := workshare.NewPool[*computeTreeSumRequest](numWorkers)
 	defer w.Close()
 
 	n := buildTree(6)
@@ -155,7 +153,7 @@ func testComputeTreeSum(t *testing.T, numWorkers int) {
 var treeToWalk = buildTree(6)
 
 func BenchmarkComputeTreeSum(b *testing.B) {
-	w := workshare.NewPool(10)
+	w := workshare.NewPool[*computeTreeSumRequest](10)
 	defer w.Close()
 
 	b.ResetTimer()
diff --git a/repo/blob/azure/azure_storage_test.go b/repo/blob/azure/azure_storage_test.go
index a09ff737..a2013f5d 100644
--- a/repo/blob/azure/azure_storage_test.go
+++ b/repo/blob/azure/azure_storage_test.go
@@ -85,7 +85,7 @@ func TestCleanupOldData(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
-	})
+	}, false)
 
 	require.NoError(t, err)
 
@@ -117,7 +117,7 @@ func TestAzureStorage(t *testing.T) {
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
 		Prefix:         fmt.Sprintf("test-%v-%x-", clock.Now().Unix(), data),
-	})
+	}, false)
 
 	cancel()
 	require.NoError(t, err)
@@ -150,7 +150,7 @@ func TestAzureStorageSASToken(t *testing.T) {
 		StorageAccount: storageAccount,
 		SASToken:       sasToken,
 		Prefix:         fmt.Sprintf("sastest-%v-%x-", clock.Now().Unix(), data),
-	})
+	}, false)
 
 	require.NoError(t, err)
 	cancel()
@@ -176,7 +176,7 @@ func TestAzureStorageInvalidBlob(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
-	})
+	}, false)
 	if err != nil {
 		t.Fatalf("unable to connect to Azure container: %v", err)
 	}
@@ -204,7 +204,7 @@ func TestAzureStorageInvalidContainer(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
-	})
+	}, false)
 
 	if err == nil {
 		t.Errorf("unexpected success connecting to Azure container, wanted error")
@@ -223,7 +223,7 @@ func TestAzureStorageInvalidCreds(t *testing.T) {
 		Container:      container,
 		StorageAccount: storageAccount,
 		StorageKey:     storageKey,
-	})
+	}, false)
 
 	if err == nil {
 		t.Errorf("unexpected success connecting to Azure blob storage, wanted error")
diff --git a/repo/blob/b2/b2_storage_test.go b/repo/blob/b2/b2_storage_test.go
index 9a289199..96a01983 100644
--- a/repo/blob/b2/b2_storage_test.go
+++ b/repo/blob/b2/b2_storage_test.go
@@ -51,7 +51,7 @@ func TestCleanupOldData(t *testing.T) {
 	}
 
 	ctx := testlogging.Context(t)
-	st, err := b2.New(ctx, opt)
+	st, err := b2.New(ctx, opt, false)
 	require.NoError(t, err)
 
 	blobtesting.CleanupOldData(ctx, t, st, blobtesting.MinCleanupAge)
@@ -76,7 +76,7 @@ func TestB2Storage(t *testing.T) {
 
 	// use context that gets canceled after opening storage to ensure it's not used beyond New().
 	newctx, cancel := context.WithCancel(ctx)
-	st, err := b2.New(newctx, opt)
+	st, err := b2.New(newctx, opt, false)
 
 	cancel()
 	require.NoError(t, err)
@@ -103,7 +103,7 @@ func TestB2StorageInvalidBlob(t *testing.T) {
 		BucketName: bucket,
 		KeyID:      keyID,
 		Key:        key,
-	})
+	}, false)
 	require.NoError(t, err)
 
 	defer st.Close(ctx)
@@ -130,7 +130,7 @@ func TestB2StorageInvalidBucket(t *testing.T) {
 		BucketName: bucket,
 		KeyID:      keyID,
 		Key:        key,
-	})
+	}, false)
 	require.Error(t, err)
 }
 
@@ -147,6 +147,6 @@ func TestB2StorageInvalidCreds(t *testing.T) {
 		BucketName: bucket,
 		KeyID:      keyID,
 		Key:        key,
-	})
+	}, false)
 	require.Error(t, err)
 }
diff --git a/repo/blob/gcs/gcs_storage_test.go b/repo/blob/gcs/gcs_storage_test.go
index 696574b2..512cd413 100644
--- a/repo/blob/gcs/gcs_storage_test.go
+++ b/repo/blob/gcs/gcs_storage_test.go
@@ -25,7 +25,7 @@ func TestCleanupOldData(t *testing.T) {
 	testutil.ProviderTest(t)
 	ctx := testlogging.Context(t)
 
-	st, err := gcs.New(ctx, mustGetOptionsOrSkip(t, ""))
+	st, err := gcs.New(ctx, mustGetOptionsOrSkip(t, ""), false)
 	require.NoError(t, err)
 
 	defer st.Close(ctx)
@@ -41,7 +41,7 @@ func TestGCSStorage(t *testing.T) {
 
 	// use context that gets canceled after opening storage to ensure it's not used beyond New().
 	newctx, cancel := context.WithCancel(ctx)
-	st, err := gcs.New(newctx, mustGetOptionsOrSkip(t, uuid.NewString()))
+	st, err := gcs.New(newctx, mustGetOptionsOrSkip(t, uuid.NewString()), false)
 
 	cancel()
 	require.NoError(t, err)
@@ -69,7 +69,7 @@ func TestGCSStorageInvalid(t *testing.T) {
 	if _, err := gcs.New(ctx, &gcs.Options{
 		BucketName:                    bucket + "-no-such-bucket",
 		ServiceAccountCredentialsFile: os.Getenv("KOPIA_GCS_CREDENTIALS_FILE"),
-	}); err == nil {
+	}, false); err == nil {
 		t.Fatalf("unexpected success connecting to GCS, wanted error")
 	}
 }
diff --git a/repo/blob/gdrive/gdrive_storage_test.go b/repo/blob/gdrive/gdrive_storage_test.go
index 6403219f..ffe3d225 100644
--- a/repo/blob/gdrive/gdrive_storage_test.go
+++ b/repo/blob/gdrive/gdrive_storage_test.go
@@ -26,7 +26,7 @@ func TestCleanupOldData(t *testing.T) {
 	testutil.ProviderTest(t)
 	ctx := testlogging.Context(t)
 
-	st, err := gdrive.New(ctx, mustGetOptionsOrSkip(t))
+	st, err := gdrive.New(ctx, mustGetOptionsOrSkip(t), false)
 	require.NoError(t, err)
 
 	defer st.Close(ctx)
@@ -44,7 +44,7 @@ func TestGDriveStorage(t *testing.T) {
 	newctx, cancel := context.WithCancel(ctx)
 	opt := mustGetOptionsOrSkip(t)
 	testOpt := createTestFolderOrSkip(newctx, t, opt, uuid.NewString())
-	st, err := gdrive.New(newctx, testOpt)
+	st, err := gdrive.New(newctx, testOpt, false)
 
 	cancel()
 	require.NoError(t, err)
@@ -72,7 +72,7 @@ func TestGdriveStorageInvalid(t *testing.T) {
 	if _, err := gdrive.New(ctx, &gdrive.Options{
 		FolderID:                      folderID + "-no-such-folder",
 		ServiceAccountCredentialsFile: os.Getenv("KOPIA_GDRIVE_CREDENTIALS_FILE"),
-	}); err == nil {
+	}, false); err == nil {
 		t.Fatalf("unexpected success connecting to Drive, wanted error")
 	}
 }
diff --git a/repo/blob/registry_test.go b/repo/blob/registry_test.go
index f482a117..60d23d73 100644
--- a/repo/blob/registry_test.go
+++ b/repo/blob/registry_test.go
@@ -23,12 +23,7 @@ type myStorage struct {
 }
 
 func TestRegistry(t *testing.T) {
-	blob.AddSupportedStorage("mystorage", func() interface{} {
-		return &myConfig{
-			Field: 3,
-		}
-	}, func(c context.Context, i interface{}, isCreate bool) (blob.Storage, error) {
-		mc := i.(*myConfig)
+	blob.AddSupportedStorage("mystorage", myConfig{Field: 3}, func(c context.Context, mc *myConfig, isCreate bool) (blob.Storage, error) {
 		return &myStorage{cfg: mc, create: isCreate}, nil
 	})
 
@@ -55,10 +50,7 @@ func TestRegistry(t *testing.T) {
 }
 
 func TestConnectionInfo(t *testing.T) {
-	blob.AddSupportedStorage("mystorage2", func() interface{} {
-		return &myConfig{}
-	}, func(c context.Context, i interface{}, isCreate bool) (blob.Storage, error) {
-		mc := i.(*myConfig)
+	blob.AddSupportedStorage("mystorage2", myConfig{}, func(c context.Context, mc *myConfig, isCreate bool) (blob.Storage, error) {
 		return &myStorage{cfg: mc}, nil
 	})
 
diff --git a/repo/blob/s3/s3_storage_test.go b/repo/blob/s3/s3_storage_test.go
index edc6ee2c..10cc8bc3 100644
--- a/repo/blob/s3/s3_storage_test.go
+++ b/repo/blob/s3/s3_storage_test.go
@@ -419,7 +419,7 @@ func TestInvalidCredsFailsFast(t *testing.T) {
 		Region:          minioRegion,
 		DoNotUseTLS:     false,
 		DoNotVerifyTLS:  false,
-	})
+	}, false)
 	require.Error(t, err)
 
 	//nolint:forbidigo
@@ -493,7 +493,7 @@ func TestNeedMD5AWS(t *testing.T) {
 
 	options.Prefix = uuid.NewString() + "/"
 
-	s, err := New(ctx, options)
+	s, err := New(ctx, options, false)
 	require.NoError(t, err, "could not create storage")
 
 	t.Cleanup(func() {
@@ -511,7 +511,7 @@ func testStorage(t *testing.T, options *Options, runValidationTest bool, opts bl
 
 	require.Equal(t, "", options.Prefix)
 
-	st0, err := New(ctx, options)
+	st0, err := New(ctx, options, false)
 
 	require.NoError(t, err)
 
@@ -523,7 +523,7 @@ func testStorage(t *testing.T, options *Options, runValidationTest bool, opts bl
 
 	// use context that gets canceled after opening storage to ensure it's not used beyond New().
 	newctx, cancel := context.WithCancel(ctx)
-	st, err := New(newctx, options)
+	st, err := New(newctx, options, false)
 
 	cancel()
 	require.NoError(t, err)
diff --git a/repo/blob/s3/s3_versioned_test.go b/repo/blob/s3/s3_versioned_test.go
index c6081221..160c4f2d 100644
--- a/repo/blob/s3/s3_versioned_test.go
+++ b/repo/blob/s3/s3_versioned_test.go
@@ -710,16 +710,9 @@ func putBlobs(ctx context.Context, tb testing.TB, s *s3Storage, blobs []blobCont
 
 // only available for tests.
 func (s *s3Storage) putBlobVersion(ctx context.Context, id blob.ID, data blob.Bytes, opts blob.PutOptions) (versionMetadata, error) {
-	var vm versionMetadata
-
-	_, err := retry.WithExponentialBackoff(ctx, "putBlobVersion("+string(id)+")", func() (interface{}, error) {
-		v, err := s.putBlob(ctx, id, data, opts)
-		vm = v
-
-		return true, err //nolint:wrapcheck
+	return retry.WithExponentialBackoff(ctx, "putBlobVersion("+string(id)+")", func() (versionMetadata, error) {
+		return s.putBlob(ctx, id, data, opts)
 	}, isRetriable)
-
-	return vm, err //nolint:wrapcheck
 }
 
 func compareMetadata(tb testing.TB, a, b versionMetadata) {
diff --git a/tests/tools/kopiaclient/kopiaclient.go b/tests/tools/kopiaclient/kopiaclient.go
index 72fbec46..359759ed 100644
--- a/tests/tools/kopiaclient/kopiaclient.go
+++ b/tests/tools/kopiaclient/kopiaclient.go
@@ -191,7 +191,7 @@ func (kc *KopiaClient) getStorage(ctx context.Context, repoDir, bucketName strin
 			AccessKeyID:     os.Getenv(awsAccessKeyIDEnvKey),
 			SecretAccessKey: os.Getenv(awsSecretAccessKeyEnvKey),
 		}
-		st, err = s3.New(ctx, s3Opts)
+		st, err = s3.New(ctx, s3Opts, false)
 	} else {
 		if iErr := os.MkdirAll(repoDir, 0o700); iErr != nil {
 			return nil, errors.Wrap(iErr, "cannot create directory")