Repository: kopia/kopia
Pull request #2610 (2022-11-30 23:03:47)

build(deps): bump github.com/klauspost/reedsolomon from 1.11.2 to 1.11.3
Bumps [github.com/klauspost/reedsolomon](https://github.com/klauspost/reedsolomon) from 1.11.2 to 1.11.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/klauspost/reedsolomon/releases">github.com/klauspost/reedsolomon's releases</a>.</em></p>
<blockquote>
<h2>BUGFIX: Fixes v1.11.2 Leopard GF8 GFNI code</h2>
<h2>What's Changed</h2>
<ul>
<li>Leopard GF8: Write directly to output by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/226">klauspost/reedsolomon#226</a></li>
<li>Disable broken Leopard GF8 GFNI code by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/229">klauspost/reedsolomon#229</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/klauspost/reedsolomon/compare/v1.11.2...v1.11.3">https://github.com/klauspost/reedsolomon/compare/v1.11.2...v1.11.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/klauspost/reedsolomon/commit/ff5c2f19439b77c0cd9064c1682dcf2a1d8b1c1f"><code>ff5c2f1</code></a> Disable broken Leopard GF8 GFNI code (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/229">#229</a>)</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/11175a9055a31d836980205e3d5eecc3cefae9bb"><code>11175a9</code></a> Leopard GF8: Write directly to output (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/226">#226</a>)</li>
<li>See full diff in <a href="https://github.com/klauspost/reedsolomon/compare/v1.11.2...v1.11.3">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/klauspost/reedsolomon&package-manager=go_modules&previous-version=1.11.2&new-version=1.11.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/kopia/kopia/pull/2610?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.86**% // Head: **74.91**% // Increases project coverage by **`+0.04%`** :tada:
> Coverage data is based on head [(`04ae6b3`)](https://codecov.io/gh/kopia/kopia/pull/2610?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`fa5aaa4`)](https://codecov.io/gh/kopia/kopia/commit/fa5aaa40ac9d8f25a0b258b71d6a8450b4409b9d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Patch has no changes to coverable lines.

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


```diff
@@            Coverage Diff             @@
##           master    #2610      +/-   ##
==========================================
+ Coverage   74.86%   74.91%   +0.04%     
==========================================
  Files         452      452              
  Lines       35478    35478              
==========================================
+ Hits        26562    26577      +15     
+ Misses       7030     7016      -14     
+ Partials     1886     1885       -1     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2610?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [snapshot/snapshotfs/upload.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-c25hcHNob3Qvc25hcHNob3Rmcy91cGxvYWQuZ28=) | `84.61% <0.00%> (-0.67%)` | :arrow_down: |
| [internal/epoch/epoch\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvZXBvY2gvZXBvY2hfbWFuYWdlci5nbw==) | `85.29% <0.00%> (+0.48%)` | :arrow_up: |
| [repo/content/index/index\_v1.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `76.17% <0.00%> (+1.17%)` | :arrow_up: |
| [repo/content/content\_manager\_iterate.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbnRlbnRfbWFuYWdlcl9pdGVyYXRlLmdv) | `90.27% <0.00%> (+2.16%)` | :arrow_up: |
| [repo/blob/throttling/throttling\_semaphore.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9ibG9iL3Rocm90dGxpbmcvdGhyb3R0bGluZ19zZW1hcGhvcmUuZ28=) | `100.00% <0.00%> (+2.94%)` | :arrow_up: |
| [cli/command\_content\_verify.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfY29udGVudF92ZXJpZnkuZ28=) | `81.51% <0.00%> (+5.88%)` | :arrow_up: |
| [internal/dirutil/mksubdirall.go](https://codecov.io/gh/kopia/kopia/pull/2610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvZGlydXRpbC9ta3N1YmRpcmFsbC5nbw==) | `100.00% <0.00%> (+7.89%)` | :arrow_up: |

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/2610?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/app/package-lock.json b/app/package-lock.json
index 24ed5f58..62397d35 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -25,8 +25,8 @@
         "electron": "^19.0.8",
         "electron-builder": "^23.3.3",
         "electron-notarize": "^1.2.2",
-        "playwright": "^1.26.1",
-        "playwright-core": "^1.26.0"
+        "playwright": "^1.28.1",
+        "playwright-core": "^1.28.1"
       }
     },
     "node_modules/@develar/schema-utils": {
@@ -211,6 +211,18 @@
         "node": ">=14"
       }
     },
+    "node_modules/@playwright/test/node_modules/playwright-core": {
+      "version": "1.26.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
+      "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+      "dev": true,
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -2813,13 +2825,13 @@
       }
     },
     "node_modules/playwright": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.26.1.tgz",
-      "integrity": "sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.1.tgz",
+      "integrity": "sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
-        "playwright-core": "1.26.1"
+        "playwright-core": "1.28.1"
       },
       "bin": {
         "playwright": "cli.js"
@@ -2829,9 +2841,9 @@
       }
     },
     "node_modules/playwright-core": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
-      "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
+      "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
       "dev": true,
       "bin": {
         "playwright": "cli.js"
@@ -3859,6 +3871,14 @@
       "requires": {
         "@types/node": "*",
         "playwright-core": "1.26.1"
+      },
+      "dependencies": {
+        "playwright-core": {
+          "version": "1.26.1",
+          "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
+          "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+          "dev": true
+        }
       }
     },
     "@sindresorhus/is": {
@@ -5900,18 +5920,18 @@
       "optional": true
     },
     "playwright": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.26.1.tgz",
-      "integrity": "sha512-WQmEdCgYYe8jOEkhkW9QLcK0PB+w1RZztBLYIT10MEEsENYg251cU0IzebDINreQsUt+HCwwRhtdz4weH9ICcQ==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.28.1.tgz",
+      "integrity": "sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q==",
       "dev": true,
       "requires": {
-        "playwright-core": "1.26.1"
+        "playwright-core": "1.28.1"
       }
     },
     "playwright-core": {
-      "version": "1.26.1",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.26.1.tgz",
-      "integrity": "sha512-hzFchhhxnEiPc4qVPs9q2ZR+5eKNifY2hQDHtg1HnTTUuphYCBP8ZRb2si+B1TR7BHirgXaPi48LIye5SgrLAA==",
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
+      "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
       "dev": true
     },
     "plist": {
diff --git a/app/package.json b/app/package.json
index 076d632a..6eab894b 100644
--- a/app/package.json
+++ b/app/package.json
@@ -117,8 +117,8 @@
     "electron": "^19.0.8",
     "electron-builder": "^23.3.3",
     "electron-notarize": "^1.2.2",
-    "playwright": "^1.26.1",
-    "playwright-core": "^1.26.0"
+    "playwright": "^1.28.1",
+    "playwright-core": "^1.28.1"
   },
   "homepage": "./",
   "description": "Fast and secure open source backup.",
diff --git a/go.mod b/go.mod
index dd10b66b..cf79b2d2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/kopia/kopia
 go 1.19
 
 require (
-	cloud.google.com/go/storage v1.28.0
+	cloud.google.com/go/storage v1.28.1
 	github.com/Azure/azure-pipeline-go v0.2.3 // indirect
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
@@ -30,7 +30,7 @@ require (
 	github.com/kylelemons/godebug v1.1.0
 	github.com/mattn/go-colorable v0.1.13
 	github.com/minio/md5-simd v1.1.2 // indirect
-	github.com/minio/minio-go/v7 v7.0.44
+	github.com/minio/minio-go/v7 v7.0.45
 	github.com/minio/sha256-simd v1.0.0 // indirect
 	github.com/natefinch/atomic v1.0.1
 	github.com/pierrec/lz4 v2.6.1+incompatible
@@ -98,7 +98,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
+	google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c // indirect
 )
 
 require (
@@ -106,7 +106,7 @@ require (
 	github.com/chromedp/chromedp v0.8.6
 	github.com/edsrzf/mmap-go v1.1.0
 	github.com/hashicorp/golang-lru v0.5.4
-	github.com/klauspost/reedsolomon v1.11.2
+	github.com/klauspost/reedsolomon v1.11.3
 	github.com/prometheus/client_model v0.3.0
 	go.opentelemetry.io/otel v1.11.1
 	go.opentelemetry.io/otel/exporters/jaeger v1.11.1
@@ -118,7 +118,7 @@ require (
 require (
 	cloud.google.com/go/compute v1.12.1 // indirect
 	cloud.google.com/go/compute/metadata v0.2.1 // indirect
-	cloud.google.com/go/iam v0.6.0 // indirect
+	cloud.google.com/go/iam v0.7.0 // indirect
 	github.com/alessio/shellescape v1.4.1 // indirect
 	github.com/chromedp/sysutil v1.0.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
diff --git a/go.sum b/go.sum
index 6023ecb6..10ac76e4 100644
--- a/go.sum
+++ b/go.sum
@@ -27,9 +27,9 @@ cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22m
 cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
-cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
-cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
+cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
+cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
-cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
+cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
+cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@@ -274,8 +274,8 @@ github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdy
 github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
-github.com/klauspost/reedsolomon v1.11.2 h1:VSMk4WXmBDbdswGyn1KNjRE0UPE9pP83KrpPZbv06Ww=
-github.com/klauspost/reedsolomon v1.11.2/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k=
+github.com/klauspost/reedsolomon v1.11.3 h1:rX9UNNvDhJ0Bq45y6uBy/eYehcjyz5faokTuZmu1Q9U=
+github.com/klauspost/reedsolomon v1.11.3/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc=
@@ -310,8 +310,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.44 h1:9zUJ7iU7ax2P1jOvTp6nVrgzlZq3AZlFm0XfRFDKstM=
-github.com/minio/minio-go/v7 v7.0.44/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
+github.com/minio/minio-go/v7 v7.0.45 h1:g4IeM9M9pW/Lo8AGGNOjBZYlvmtlE1N5TQEYWXRWzIs=
+github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
 github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
 github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -721,8 +721,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
-google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c h1:S34D59DS2GWOEwWNt4fYmTcFrtlOgukG2k9WsomZ7tg=
+google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/internal/cache/content_cache.go b/internal/cache/content_cache.go
index 74b5a5f0..c3d936eb 100644
--- a/internal/cache/content_cache.go
+++ b/internal/cache/content_cache.go
@@ -5,6 +5,7 @@
 
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/impossible"
 	"github.com/kopia/kopia/internal/metrics"
@@ -189,7 +190,7 @@ func NewContentCache(ctx context.Context, st blob.Storage, opt Options, mr *metr
 		}
 	}
 
-	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, ChecksumProtection(opt.HMACSecret), opt.Sweep, mr)
+	pc, err := NewPersistentCache(ctx, opt.CacheSubDir, cacheStorage, cacheprot.ChecksumProtection(opt.HMACSecret), opt.Sweep, mr)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to create base cache")
 	}
diff --git a/internal/cache/persistent_lru_cache.go b/internal/cache/persistent_lru_cache.go
index 6b539fab..09d0f8af 100644
--- a/internal/cache/persistent_lru_cache.go
+++ b/internal/cache/persistent_lru_cache.go
@@ -11,6 +11,7 @@
 	lru "github.com/hashicorp/golang-lru"
 	"github.com/pkg/errors"
 
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/ctxutil"
 	"github.com/kopia/kopia/internal/gather"
@@ -42,7 +43,7 @@ type PersistentCache struct {
 	anyChange atomic.Bool
 
 	cacheStorage      Storage
-	storageProtection StorageProtection
+	storageProtection cacheprot.StorageProtection
 	sweep             SweepSettings
 
 	description string
@@ -137,7 +138,7 @@ func (c *PersistentCache) GetPartial(ctx context.Context, key string, offset, le
 		prot := c.storageProtection
 		if length >= 0 {
 			// only full items have protection.
-			prot = nullStorageProtection{}
+			prot = cacheprot.NoProtection()
 		}
 
 		if err := prot.Verify(key, tmp.Bytes(), output); err == nil {
@@ -333,7 +334,7 @@ func (s SweepSettings) applyDefaults() SweepSettings {
 }
 
 // NewPersistentCache creates the persistent cache in the provided storage.
-func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection StorageProtection, sweep SweepSettings, mr *metrics.Registry) (*PersistentCache, error) {
+func NewPersistentCache(ctx context.Context, description string, cacheStorage Storage, storageProtection cacheprot.StorageProtection, sweep SweepSettings, mr *metrics.Registry) (*PersistentCache, error) {
 	if cacheStorage == nil {
 		return nil, nil
 	}
@@ -341,7 +342,7 @@ func NewPersistentCache(ctx context.Context, description string, cacheStorage St
 	sweep = sweep.applyDefaults()
 
 	if storageProtection == nil {
-		storageProtection = NoProtection()
+		storageProtection = cacheprot.NoProtection()
 	}
 
 	c := &PersistentCache{
diff --git a/internal/cache/storage_protection.go b/internal/cacheprot/storage_protection.go
similarity index 97%
rename from internal/cache/storage_protection.go
rename to internal/cacheprot/storage_protection.go
index 947a45c7..77658215 100644
--- a/internal/cache/storage_protection.go
+++ b/internal/cacheprot/storage_protection.go
@@ -1,4 +1,5 @@
-package cache
+// Package cacheprot provides utilities for protection of cache entries.
+package cacheprot
 
 import (
 	"crypto/sha256"
diff --git a/internal/metricid/id_mapping.go b/internal/metricid/id_mapping.go
new file mode 100644
index 00000000..6818b2c1
--- /dev/null
+++ b/internal/metricid/id_mapping.go
@@ -0,0 +1,70 @@
+// Package metricid provides mapping between metric names and persistent IDs.
+package metricid
+
+// Mapping contains translation of names to indexes and vice versa, which allows maps
+// of well-known keys to be converted to slices of just values with more compact JSON representations.
+type Mapping struct {
+	MaxIndex    int
+	NameToIndex map[string]int
+	IndexToName map[int]string
+}
+
+// MapToSlice converts the given map to a slice according to the provided mapping.
+// Keys not in the mapping are dropped.
+func MapToSlice[T any](mapping *Mapping, input map[string]T) []T {
+	result := make([]T, mapping.MaxIndex)
+
+	for k, v := range input {
+		id := mapping.NameToIndex[k]
+		if id > 0 {
+			result[id-1] = v
+		}
+	}
+
+	return result
+}
+
+// SliceToMap converts the given slice to a map according to the provided mapping.
+func SliceToMap[T any](mapping *Mapping, input []T) map[string]T {
+	result := map[string]T{}
+
+	for k, v := range input {
+		if n, ok := mapping.IndexToName[k+1]; ok {
+			result[n] = v
+		}
+	}
+
+	return result
+}
+
+// NewMapping creates a new Mapping given the provided name-to-index map.
+func NewMapping(fwd map[string]int) *Mapping {
+	m := &Mapping{
+		NameToIndex: fwd,
+		IndexToName: inverse(fwd),
+	}
+
+	max := 0
+
+	for _, index := range fwd {
+		if index > max {
+			max = index
+		}
+	}
+
+	m.MaxIndex = max
+
+	return m
+}
+
+func inverse(m map[string]int) map[int]string {
+	res := map[int]string{}
+
+	for k, v := range m {
+		if v > 0 {
+			res[v] = k
+		}
+	}
+
+	return res
+}
diff --git a/internal/metricid/metricid.go b/internal/metricid/metricid.go
new file mode 100644
index 00000000..14acf326
--- /dev/null
+++ b/internal/metricid/metricid.go
@@ -0,0 +1,67 @@
+// Package metricid provides mapping between metric names and persistent IDs.
+package metricid
+
+// Counters contains a mapping of counter names to ID.
+//
+//nolint:gochecknoglobals,gomnd
+var Counters = NewMapping(map[string]int{
+	"blob_download_full_blob_bytes":                1,
+	"blob_download_partial_blob_bytes":             2,
+	"blob_errors[method:Close]":                    3,
+	"blob_errors[method:DeleteBlob]":               4,
+	"blob_errors[method:FlushCaches]":              5,
+	"blob_errors[method:GetBlob]":                  6,
+	"blob_errors[method:GetCapacity]":              7,
+	"blob_errors[method:GetMetadata]":              8,
+	"blob_errors[method:ListBlobs]":                9,
+	"blob_errors[method:PutBlob]":                  10,
+	"blob_list_items":                              11,
+	"blob_upload_bytes":                            12,
+	"content_after_compression_bytes":              13,
+	"content_compressible_bytes":                   14,
+	"content_compression_attempted_bytes":          15,
+	"content_compression_attempted_duration_nanos": 16,
+	"content_compression_savings_bytes":            17,
+	"content_decompressed_bytes":                   18,
+	"content_decompressed_duration_nanos":          19,
+	"content_decrypted_bytes":                      20,
+	"content_decrypted_duration_nanos":             21,
+	"content_deduplicated":                         22,
+	"content_deduplicated_bytes":                   23,
+	"content_encrypted_bytes":                      24,
+	"content_encrypted_duration_nanos":             25,
+	"content_get_error_count":                      26,
+	"content_get_not_found_count":                  27,
+	"content_hashed_bytes":                         28,
+	"content_hashed_duration_nanos":                29,
+	"content_non_compressible_bytes":               30,
+	"content_read_bytes":                           31,
+	"content_read_duration_nanos":                  32,
+	"content_uploaded_bytes":                       33,
+	"content_write_bytes":                          34,
+	"content_write_duration_nanos":                 35,
+	// add new items here, use consecutive values
+})
+
+// DurationDistributions contains a mapping of DurationDistribution names to ID.
+//
+//nolint:gochecknoglobals,gomnd
+var DurationDistributions = NewMapping(map[string]int{
+	"blob_storage_latency[method:Close]":           1,
+	"blob_storage_latency[method:DeleteBlob]":      2,
+	"blob_storage_latency[method:FlushCaches]":     3,
+	"blob_storage_latency[method:GetBlob-full]":    4,
+	"blob_storage_latency[method:GetBlob-partial]": 5,
+	"blob_storage_latency[method:GetCapacity]":     6,
+	"blob_storage_latency[method:GetMetadata]":     7,
+	"blob_storage_latency[method:ListBlobs]":       8,
+	"blob_storage_latency[method:PutBlob]":         9,
+	// add new items here, use consecutive values
+})
+
+// SizeDistributions provides mapping between SizeDistribution metric names to IDs.
+//
+//nolint:gochecknoglobals
+var SizeDistributions = NewMapping(map[string]int{
+	// add new items here, use consecutive values
+})
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index 7f4c68d3..e0e1b38c 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -34,8 +34,9 @@ type apiServerRepository struct {
 	serverSupportsContentCompression bool
 	omgr                             *object.Manager
 	wso                              WriteSessionOptions
+	afterFlush                       []RepositoryWriterCallback
 
-	immutableServerRepositoryParameters // immutable parameters
+	*immutableServerRepositoryParameters // immutable parameters
 }
 
 func (r *apiServerRepository) APIServerURL() string {
@@ -132,7 +133,19 @@ func (r *apiServerRepository) Refresh(ctx context.Context) error {
 }
 
 func (r *apiServerRepository) Flush(ctx context.Context) error {
-	return errors.Wrap(r.cli.Post(ctx, "flush", nil, nil), "Flush")
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
+	if err := r.cli.Post(ctx, "flush", nil, nil); err != nil {
+		return errors.Wrap(err, "Flush")
+	}
+
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 func (r *apiServerRepository) SupportsContentCompression() (bool, error) {
@@ -152,6 +165,11 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
 
 	w.omgr = omgr
 	w.wso = opt
+	w.afterFlush = nil
+
+	if w.wso.OnUpload == nil {
+		w.wso.OnUpload = func(i int64) {}
+	}
 
 	r.addRef()
 
@@ -255,10 +273,15 @@ func (r *apiServerRepository) PrefetchContents(ctx context.Context, contentIDs [
 	return resp.ContentIDs
 }
 
+// OnSuccessfulFlush registers the provided callback to be invoked after flush succeeds.
+func (r *apiServerRepository) OnSuccessfulFlush(callback RepositoryWriterCallback) {
+	r.afterFlush = append(r.afterFlush, callback)
+}
+
 var _ Repository = (*apiServerRepository)(nil)
 
 // openRestAPIRepository connects remote repository over Kopia API.
-func openRestAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
+func openRestAPIRepository(ctx context.Context, si *APIServerInfo, password string, par *immutableServerRepositoryParameters) (Repository, error) {
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index 9800bf82..1aa99fa7 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -11,6 +11,7 @@
 	"go.uber.org/zap"
 
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/gather"
@@ -427,7 +428,7 @@ func (sm *SharedManager) setupReadManagerCaches(ctx context.Context, caching *Ca
 		return errors.Wrap(err, "unable to initialize index blob cache storage")
 	}
 
-	indexBlobCache, err := cache.NewPersistentCache(ctx, "index-blobs", indexBlobStorage, cache.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
+	indexBlobCache, err := cache.NewPersistentCache(ctx, "index-blobs", indexBlobStorage, cacheprot.ChecksumProtection(caching.HMACSecret), cache.SweepSettings{
 		MaxSizeBytes: metadataCacheSize,
 		MinSweepAge:  caching.MinMetadataSweepAge.DurationOrDefault(DefaultMetadataCacheSweepAge),
 	}, mr)
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index f3bdf9a9..ae088a92 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -67,13 +67,15 @@ type grpcRepositoryClient struct {
 	isReadOnly         bool
 	transparentRetries bool
 
+	afterFlush []RepositoryWriterCallback
+
 	// how many times we tried to establish inner session
 	// +checklocks:innerSessionMutex
 	innerSessionAttemptCount int
 
 	asyncWritesWG *errgroup.Group
 
-	immutableServerRepositoryParameters
+	*immutableServerRepositoryParameters
 
 	serverSupportsContentCompression bool
 	omgr                             *object.Manager
@@ -422,11 +424,21 @@ func (r *grpcRepositoryClient) Flush(ctx context.Context) error {
 		return errors.Wrap(err, "error waiting for async writes")
 	}
 
-	_, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
+	if _, err := inSessionWithoutRetry(ctx, r, func(ctx context.Context, sess *grpcInnerSession) (bool, error) {
 		return false, sess.Flush(ctx)
-	})
+	}); err != nil {
+		return err
+	}
 
-	return err
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 func (r *grpcInnerSession) Flush(ctx context.Context) error {
@@ -716,6 +728,11 @@ func (r *grpcRepositoryClient) UpdateDescription(d string) {
 	r.cliOpts.Description = d
 }
 
+// OnSuccessfulFlush registers the provided callback to be invoked after flush succeeds.
+func (r *grpcRepositoryClient) OnSuccessfulFlush(callback RepositoryWriterCallback) {
+	r.afterFlush = append(r.afterFlush, callback)
+}
+
 var _ Repository = (*grpcRepositoryClient)(nil)
 
 type grpcCreds struct {
@@ -743,7 +760,7 @@ func (c grpcCreds) RequireTransportSecurity() bool {
 
 // openGRPCAPIRepository opens the Repository based on remote GRPC server.
 // The APIServerInfo must have the address of the repository as 'https://host:port'
-func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password string, par immutableServerRepositoryParameters) (Repository, error) {
+func openGRPCAPIRepository(ctx context.Context, si *APIServerInfo, password string, par *immutableServerRepositoryParameters) (Repository, error) {
 	var transportCreds credentials.TransportCredentials
 
 	if si.TrustedServerCertificateFingerprint != "" {
@@ -852,7 +869,7 @@ func newGRPCAPIRepositoryForConnection(
 	conn *grpc.ClientConn,
 	opt WriteSessionOptions,
 	transparentRetries bool,
-	par immutableServerRepositoryParameters,
+	par *immutableServerRepositoryParameters,
 ) (*grpcRepositoryClient, error) {
 	if opt.OnUpload == nil {
 		opt.OnUpload = func(i int64) {}
diff --git a/repo/open.go b/repo/open.go
index d44e73cf..f691a7ee 100644
--- a/repo/open.go
+++ b/repo/open.go
@@ -13,6 +13,7 @@
 	"golang.org/x/crypto/scrypt"
 
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/feature"
 	"github.com/kopia/kopia/internal/metrics"
@@ -64,11 +65,12 @@
 
 // Options provides configuration parameters for connection to a repository.
 type Options struct {
-	TraceStorage        bool             // Logs all storage access using provided Printf-style function
-	TimeNowFunc         func() time.Time // Time provider
-	DisableInternalLog  bool             // Disable internal log
-	UpgradeOwnerID      string           // Owner-ID of any upgrade in progress, when this is not set the access may be restricted
-	DoNotWaitForUpgrade bool             // Disable the exponential forever backoff on an upgrade lock.
+	TraceStorage        bool                       // Logs all storage access using provided Printf-style function
+	TimeNowFunc         func() time.Time           // Time provider
+	DisableInternalLog  bool                       // Disable internal log
+	UpgradeOwnerID      string                     // Owner-ID of any upgrade in progress, when this is not set the access may be restricted
+	DoNotWaitForUpgrade bool                       // Disable the exponential forever backoff on an upgrade lock.
+	BeforeFlush         []RepositoryWriterCallback // list of callbacks to invoke before every flush
 
 	OnFatalError func(err error) // function to invoke when repository encounters a fatal error, usually invokes os.Exit
 
@@ -119,7 +121,7 @@ func Open(ctx context.Context, configFile, password string, options *Options) (r
 	}
 
 	if lc.APIServer != nil {
-		return OpenAPIServer(ctx, lc.APIServer, lc.ClientOptions, lc.Caching, password)
+		return openAPIServer(ctx, lc.APIServer, lc.ClientOptions, lc.Caching, password, options)
 	}
 
 	return openDirect(ctx, configFile, lc, password, options)
@@ -143,7 +145,7 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 		return nil, errors.Wrap(err, "unable to derive cache encryption key from password")
 	}
 
-	prot, err := cache.AuthenticatedEncryptionProtection(cacheEncryptionKey)
+	prot, err := cacheprot.AuthenticatedEncryptionProtection(cacheEncryptionKey)
 	if err != nil {
 		return nil, errors.Wrap(err, "unable to initialize protection")
 	}
@@ -159,8 +161,10 @@ func getContentCacheOrNil(ctx context.Context, opt *content.CachingOptions, pass
 	return pc, nil
 }
 
-// OpenAPIServer connects remote repository over Kopia API.
-func OpenAPIServer(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, cachingOptions *content.CachingOptions, password string) (Repository, error) {
+// openAPIServer connects remote repository over Kopia API.
+func openAPIServer(ctx context.Context, si *APIServerInfo, cliOpts ClientOptions, cachingOptions *content.CachingOptions, password string, options *Options) (Repository, error) {
+	cachingOptions = cachingOptions.CloneOrDefault()
+
 	mr := metrics.NewRegistry()
 
 	contentCache, err := getContentCacheOrNil(ctx, cachingOptions, password, mr)
@@ -179,11 +183,12 @@ func(ctx context.Context) error {
 		mr.Close,
 	)
 
-	par := immutableServerRepositoryParameters{
+	par := &immutableServerRepositoryParameters{
 		cliOpts:          cliOpts,
 		contentCache:     contentCache,
 		metricsRegistry:  mr,
 		refCountedCloser: closer,
+		beforeFlush:      options.BeforeFlush,
 	}
 
 	if si.DisableGRPC {
@@ -345,6 +350,7 @@ func openWithConfig(ctx context.Context, st blob.Storage, cliOpts ClientOptions,
 			throttler:        throttler,
 			metricsRegistry:  mr,
 			refCountedCloser: closer,
+			beforeFlush:      options.BeforeFlush,
 		},
 	}
 
diff --git a/repo/repository.go b/repo/repository.go
index ad60b847..77e1d2e2 100644
--- a/repo/repository.go
+++ b/repo/repository.go
@@ -49,6 +49,7 @@ type RepositoryWriter interface {
 	ConcatenateObjects(ctx context.Context, objectIDs []object.ID) (object.ID, error)
 	PutManifest(ctx context.Context, labels map[string]string, payload interface{}) (manifest.ID, error)
 	DeleteManifest(ctx context.Context, id manifest.ID) error
+	OnSuccessfulFlush(callback RepositoryWriterCallback)
 	Flush(ctx context.Context) error
 }
 
@@ -97,10 +98,24 @@ type immutableDirectRepositoryParameters struct {
 	nextWriterID    *int32
 	throttler       throttling.SettableThrottler
 	metricsRegistry *metrics.Registry
+	beforeFlush     []RepositoryWriterCallback
 
 	*refCountedCloser
 }
 
+// RepositoryWriterCallback is a hook function invoked before and after each flush.
+type RepositoryWriterCallback func(ctx context.Context, w RepositoryWriter) error
+
+func invokeCallbacks(ctx context.Context, w RepositoryWriter, callbacks []RepositoryWriterCallback) error {
+	for _, h := range callbacks {
+		if err := h(ctx, w); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // directRepository is an implementation of repository that directly manipulates underlying storage.
 type directRepository struct {
 	immutableDirectRepositoryParameters
@@ -110,6 +125,8 @@ type directRepository struct {
 	omgr  *object.Manager
 	mmgr  *manifest.Manager
 	sm    *content.SharedManager
+
+	afterFlush []RepositoryWriterCallback
 }
 
 // DeriveKey derives encryption key of the provided length from the master key.
@@ -277,11 +294,23 @@ func (r *directRepository) NewDirectWriter(ctx context.Context, opt WriteSession
 
 // Flush waits for all in-flight writes to complete.
 func (r *directRepository) Flush(ctx context.Context) error {
+	if err := invokeCallbacks(ctx, r, r.beforeFlush); err != nil {
+		return errors.Wrap(err, "before flush")
+	}
+
 	if err := r.mmgr.Flush(ctx); err != nil {
 		return errors.Wrap(err, "error flushing manifests")
 	}
 
-	return errors.Wrap(r.cmgr.Flush(ctx), "error flushing contents")
+	if err := r.cmgr.Flush(ctx); err != nil {
+		return errors.Wrap(err, "error flushing contents")
+	}
+
+	if err := invokeCallbacks(ctx, r, r.afterFlush); err != nil {
+		return errors.Wrap(err, "after flush")
+	}
+
+	return nil
 }
 
 // Metrics provides access to metrics registry.
@@ -335,6 +364,11 @@ func (r *directRepository) FormatManager() *format.Manager {
 	return r.fmgr
 }
 
+// OnSuccessfulFlush registers the provided callback to be invoked after flush succeeds.
+func (r *directRepository) OnSuccessfulFlush(callback RepositoryWriterCallback) {
+	r.afterFlush = append(r.afterFlush, callback)
+}
+
 // WriteSessionOptions describes options for a write session.
 type WriteSessionOptions struct {
 	Purpose        string
diff --git a/repo/server_repository_params.go b/repo/server_repository_params.go
index cd870d37..93ef1a77 100644
--- a/repo/server_repository_params.go
+++ b/repo/server_repository_params.go
@@ -14,6 +14,8 @@ type immutableServerRepositoryParameters struct {
 	cliOpts         ClientOptions
 	metricsRegistry *metrics.Registry
 	contentCache    *cache.PersistentCache
+	beforeFlush     []RepositoryWriterCallback
+
 	*refCountedCloser
 }
 


Test patch:
diff --git a/internal/acl/access_level_test.go b/internal/acl/access_level_test.go
index 7e0afd70..e55d4bc7 100644
--- a/internal/acl/access_level_test.go
+++ b/internal/acl/access_level_test.go
@@ -5,6 +5,7 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/require"
 
 	"github.com/kopia/kopia/internal/acl"
 )
@@ -23,9 +24,7 @@ func TestAccessLevelJSONSerialization(t *testing.T) {
 	s1.E = acl.AccessLevelFull
 
 	v, err := json.MarshalIndent(s1, "", "  ")
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	got := string(v)
 	want := `{
diff --git a/internal/cache/persistent_lru_cache_test.go b/internal/cache/persistent_lru_cache_test.go
index 24dec137..76ee7ee5 100644
--- a/internal/cache/persistent_lru_cache_test.go
+++ b/internal/cache/persistent_lru_cache_test.go
@@ -11,6 +11,7 @@
 
 	"github.com/kopia/kopia/internal/blobtesting"
 	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/fault"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/testlogging"
@@ -25,7 +26,7 @@ func TestPersistentLRUCache(t *testing.T) {
 
 	cs := blobtesting.NewMapStorage(blobtesting.DataMap{}, nil, nil).(cache.Storage)
 
-	pc, err := cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -67,7 +68,7 @@ func TestPersistentLRUCache(t *testing.T) {
 	verifyBlobExists(ctx, t, cs, "key3")
 	verifyBlobExists(ctx, t, cs, "key4")
 
-	pc, err = cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err = cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -81,7 +82,7 @@ func TestPersistentLRUCache(t *testing.T) {
 
 	// create another persistent cache based on the same storage but wrong protection key.
 	// all reads from cache will be invalid, which means GetOrLoad will fetch them from the source.
-	pc2, err := cache.NewPersistentCache(ctx, "testing", cs, cache.ChecksumProtection([]byte{3, 2, 1}), cache.SweepSettings{
+	pc2, err := cache.NewPersistentCache(ctx, "testing", cs, cacheprot.ChecksumProtection([]byte{3, 2, 1}), cache.SweepSettings{
 		MaxSizeBytes:   maxSizeBytes,
 		TouchThreshold: cache.DefaultTouchThreshold,
 		SweepFrequency: cache.DefaultSweepFrequency,
@@ -153,7 +154,7 @@ func TestPersistentLRUCache_GetDeletesInvalidBlob(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	pc.Put(ctx, "key", gather.FromSlice([]byte{1, 2, 3}))
@@ -183,7 +184,7 @@ func TestPersistentLRUCache_PutIgnoresStorageFailure(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{}, nil)
 	require.NoError(t, err)
 
 	fs.AddFault(blobtesting.MethodPutBlob).ErrorInstead(someError)
@@ -209,7 +210,7 @@ func TestPersistentLRUCache_SweepMinSweepAge(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
 		MinSweepAge:    10 * time.Second,
@@ -238,7 +239,7 @@ func TestPersistentLRUCache_SweepIgnoresErrors(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 100 * time.Millisecond,
 		MaxSizeBytes:   1000,
 	}, nil)
@@ -271,7 +272,7 @@ func TestPersistentLRUCache_Sweep1(t *testing.T) {
 	fs := blobtesting.NewFaultyStorage(st)
 	fc := faultyCache{fs}
 
-	pc, err := cache.NewPersistentCache(ctx, "test", fc, cache.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
+	pc, err := cache.NewPersistentCache(ctx, "test", fc, cacheprot.ChecksumProtection([]byte{1, 2, 3}), cache.SweepSettings{
 		SweepFrequency: 10 * time.Millisecond,
 		MaxSizeBytes:   1,
 		MinSweepAge:    0 * time.Second,
diff --git a/internal/cache/storage_protection_test.go b/internal/cacheprot/storage_protection_test.go
similarity index 74%
rename from internal/cache/storage_protection_test.go
rename to internal/cacheprot/storage_protection_test.go
index 4f6655a4..31d5ee11 100644
--- a/internal/cache/storage_protection_test.go
+++ b/internal/cacheprot/storage_protection_test.go
@@ -1,4 +1,4 @@
-package cache_test
+package cacheprot_test
 
 import (
 	"bytes"
@@ -6,29 +6,27 @@
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/kopia/kopia/internal/cache"
+	"github.com/kopia/kopia/internal/cacheprot"
 	"github.com/kopia/kopia/internal/gather"
 )
 
 func TestNoStorageProection(t *testing.T) {
-	testStorageProtection(t, cache.NoProtection(), false)
+	testStorageProtection(t, cacheprot.NoProtection(), false)
 }
 
 func TestHMACStorageProtection(t *testing.T) {
-	testStorageProtection(t, cache.ChecksumProtection([]byte{1, 2, 3, 4}), true)
+	testStorageProtection(t, cacheprot.ChecksumProtection([]byte{1, 2, 3, 4}), true)
 }
 
 func TestEncryptionStorageProtection(t *testing.T) {
-	e, err := cache.AuthenticatedEncryptionProtection([]byte{1})
-	if err != nil {
-		t.Fatal(err)
-	}
+	e, err := cacheprot.AuthenticatedEncryptionProtection([]byte{1})
+	require.NoError(t, err)
 
 	testStorageProtection(t, e, true)
 }
 
 //nolint:thelper
-func testStorageProtection(t *testing.T, sp cache.StorageProtection, protectsFromBitFlips bool) {
+func testStorageProtection(t *testing.T, sp cacheprot.StorageProtection, protectsFromBitFlips bool) {
 	payload := []byte{0, 1, 2, 3, 4}
 
 	var protected gather.WriteBuffer
diff --git a/internal/metricid/id_mapping_test.go b/internal/metricid/id_mapping_test.go
new file mode 100644
index 00000000..5ebbb0ba
--- /dev/null
+++ b/internal/metricid/id_mapping_test.go
@@ -0,0 +1,54 @@
+package metricid_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metricid"
+)
+
+func TestMapToSlice(t *testing.T) {
+	m := metricid.NewMapping(map[string]int{
+		"a": 1,
+		"b": 2,
+		"d": 4,
+	})
+
+	cases := []struct {
+		input map[string]string
+		want  []string
+	}{
+		{nil, []string{"", "", "", ""}},
+		{map[string]string{"a": "foo", "b": "bar"}, []string{"foo", "bar", "", ""}},
+		{map[string]string{"a": "foo", "b": "bar", "d": "baz"}, []string{"foo", "bar", "", "baz"}},
+
+		// 'c' is dropped since it's not in the mapping
+		{map[string]string{"a": "foo", "b": "bar", "c": "qux", "d": "baz"}, []string{"foo", "bar", "", "baz"}},
+	}
+
+	for _, tc := range cases {
+		require.Equal(t, tc.want, metricid.MapToSlice(m, tc.input))
+	}
+}
+
+func TestSliceToMap(t *testing.T) {
+	m := metricid.NewMapping(map[string]int{
+		"a": 1,
+		"b": 2,
+		"c": 3,
+	})
+
+	cases := []struct {
+		input []int
+		want  map[string]int
+	}{
+		{[]int{3}, map[string]int{"a": 3}},
+		{[]int{3, 4, 5}, map[string]int{"a": 3, "b": 4, "c": 5}},
+		{[]int{3, 4, 5, 6, 7}, map[string]int{"a": 3, "b": 4, "c": 5}},
+	}
+
+	for _, tc := range cases {
+		require.Equal(t, tc.want, metricid.SliceToMap(m, tc.input))
+	}
+}
diff --git a/internal/metricid/metricid_test.go b/internal/metricid/metricid_test.go
new file mode 100644
index 00000000..8e06097f
--- /dev/null
+++ b/internal/metricid/metricid_test.go
@@ -0,0 +1,39 @@
+package metricid_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/metricid"
+)
+
+func TestMappings(t *testing.T) {
+	verifyMapping(t, metricid.Counters)
+	verifyMapping(t, metricid.DurationDistributions)
+	verifyMapping(t, metricid.SizeDistributions)
+}
+
+func verifyMapping(t *testing.T, mapping *metricid.Mapping) {
+	t.Helper()
+
+	id2name := map[int]string{}
+	maxv := 0
+
+	for k, v := range mapping.NameToIndex {
+		_, ok := id2name[v]
+		require.False(t, ok, "duplicate ID", v)
+
+		if v > maxv {
+			maxv = v
+		}
+
+		id2name[v] = k
+
+		require.Equal(t, k, mapping.IndexToName[v])
+	}
+
+	// make sure we use consecurive numbers
+	require.Equal(t, maxv, len(id2name))
+	require.Equal(t, mapping.MaxIndex, maxv)
+}
diff --git a/internal/repotesting/repotesting_test.go b/internal/repotesting/repotesting_test.go
index faeb3963..68a53397 100644
--- a/internal/repotesting/repotesting_test.go
+++ b/internal/repotesting/repotesting_test.go
@@ -4,6 +4,8 @@
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/kopia/kopia/internal/faketime"
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/internal/mockfs"
@@ -28,9 +30,7 @@ func TestTimeFuncWiring(t *testing.T) {
 	r0 := rep.(repo.DirectRepository)
 
 	_, env.RepositoryWriter, err = r0.NewDirectWriter(ctx, repo.WriteSessionOptions{Purpose: "test"})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	// verify wiring for the repo layer
 	if got, want := env.RepositoryWriter.Time(), ft.NowFunc()(); !got.Equal(want) {
diff --git a/internal/server/api_cli_test.go b/internal/server/api_cli_test.go
index ddad1f27..72995e48 100644
--- a/internal/server/api_cli_test.go
+++ b/internal/server/api_cli_test.go
@@ -9,17 +9,18 @@
 	"github.com/kopia/kopia/internal/apiclient"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 )
 
 func TestCLIAPI(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_paths_test.go b/internal/server/api_paths_test.go
index 202e947f..748e7a37 100644
--- a/internal/server/api_paths_test.go
+++ b/internal/server/api_paths_test.go
@@ -8,18 +8,19 @@
 	"github.com/kopia/kopia/internal/apiclient"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testutil"
 )
 
 func TestPathsAPI(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_policies_test.go b/internal/server/api_policies_test.go
index 7e9cc724..12b6e52b 100644
--- a/internal/server/api_policies_test.go
+++ b/internal/server/api_policies_test.go
@@ -11,6 +11,7 @@
 	"github.com/kopia/kopia/internal/apiclient"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/repo/compression"
 	"github.com/kopia/kopia/snapshot"
@@ -19,13 +20,13 @@
 
 func TestPolicies(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_restore_test.go b/internal/server/api_restore_test.go
index e84427e2..2a1c01f3 100644
--- a/internal/server/api_restore_test.go
+++ b/internal/server/api_restore_test.go
@@ -14,6 +14,7 @@
 	"github.com/kopia/kopia/internal/mockfs"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/internal/uitask"
 	"github.com/kopia/kopia/repo"
@@ -46,13 +47,13 @@ func TestRestoreSnapshots(t *testing.T) {
 		return nil
 	}))
 
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_snapshots_test.go b/internal/server/api_snapshots_test.go
index e3b20dfa..065a77b4 100644
--- a/internal/server/api_snapshots_test.go
+++ b/internal/server/api_snapshots_test.go
@@ -10,6 +10,7 @@
 	"github.com/kopia/kopia/internal/mockfs"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/manifest"
 	"github.com/kopia/kopia/snapshot"
@@ -72,13 +73,13 @@ func TestListAndDeleteSnapshots(t *testing.T) {
 		return nil
 	}))
 
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
@@ -201,13 +202,13 @@ func TestEditSnapshots(t *testing.T) {
 		return nil
 	}))
 
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_sources_test.go b/internal/server/api_sources_test.go
index 356b981c..45de5e56 100644
--- a/internal/server/api_sources_test.go
+++ b/internal/server/api_sources_test.go
@@ -12,6 +12,7 @@
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/internal/uitask"
 	"github.com/kopia/kopia/snapshot"
@@ -20,13 +21,13 @@
 
 func TestSnapshotCounters(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
@@ -98,15 +99,15 @@ func TestSnapshotCounters(t *testing.T) {
 
 func TestSourceRefreshesAfterPolicy(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	_ = ctx
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/api_ui_pref_test.go b/internal/server/api_ui_pref_test.go
index 21262a4e..02b3d0b2 100644
--- a/internal/server/api_ui_pref_test.go
+++ b/internal/server/api_ui_pref_test.go
@@ -8,17 +8,18 @@
 	"github.com/kopia/kopia/internal/apiclient"
 	"github.com/kopia/kopia/internal/repotesting"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 )
 
 func TestUIPreferences(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	srvInfo := startServer(t, env, false)
+	srvInfo := servertesting.StartServer(t, env, false)
 
 	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             srvInfo.BaseURL,
 		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index f85d8ea0..60803cca 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -2,26 +2,17 @@
 
 import (
 	"context"
-	"crypto/sha256"
-	"encoding/hex"
 	"io"
 	"net/http"
-	"net/http/httptest"
-	"path/filepath"
 	"testing"
-	"time"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/gorilla/mux"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/require"
 
 	"github.com/kopia/kopia/internal/apiclient"
-	"github.com/kopia/kopia/internal/auth"
-	"github.com/kopia/kopia/internal/passwordpersist"
 	"github.com/kopia/kopia/internal/repotesting"
-	"github.com/kopia/kopia/internal/server"
-	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/content"
@@ -31,70 +22,11 @@
 )
 
 const (
-	testUsername = "foo"
-	testHostname = "bar"
-	testPassword = "123"
 	testPathname = "/tmp/path"
 
-	testUIUsername = "ui-user"
-	testUIPassword = "123456"
-
 	maxCacheSizeBytes = 1e6
 )
 
-//nolint:thelper
-func startServer(t *testing.T, env *repotesting.Environment, tls bool) *repo.APIServerInfo {
-	ctx := testlogging.Context(t)
-
-	s, err := server.New(ctx, &server.Options{
-		ConfigFile:      env.ConfigFile(),
-		PasswordPersist: passwordpersist.File(),
-		Authorizer:      auth.LegacyAuthorizer(),
-		Authenticator: auth.CombineAuthenticators(
-			auth.AuthenticateSingleUser(testUsername+"@"+testHostname, testPassword),
-			auth.AuthenticateSingleUser(testUIUsername, testUIPassword),
-		),
-		RefreshInterval:   1 * time.Minute,
-		UIUser:            testUIUsername,
-		UIPreferencesFile: filepath.Join(testutil.TempDirectory(t), "ui-pref.json"),
-	})
-
-	require.NoError(t, err)
-
-	s.SetRepository(ctx, env.Repository)
-
-	// ensure we disconnect the repository before shutting down the server.
-	t.Cleanup(func() { s.SetRepository(ctx, nil) })
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	asi := &repo.APIServerInfo{}
-
-	m := mux.NewRouter()
-	s.SetupHTMLUIAPIHandlers(m)
-	s.SetupRepositoryAPIHandlers(m)
-	s.SetupControlAPIHandlers(m)
-	s.ServeStaticFiles(m, server.AssetFile())
-
-	hs := httptest.NewUnstartedServer(s.GRPCRouterHandler(m))
-	if tls {
-		hs.EnableHTTP2 = true
-		hs.StartTLS()
-		serverHash := sha256.Sum256(hs.Certificate().Raw)
-		asi.BaseURL = hs.URL
-		asi.TrustedServerCertificateFingerprint = hex.EncodeToString(serverHash[:])
-	} else {
-		hs.Start()
-		asi.BaseURL = hs.URL
-	}
-
-	t.Cleanup(hs.Close)
-
-	return asi
-}
-
 func TestServer_REST(t *testing.T) {
 	testServer(t, true)
 }
@@ -106,27 +38,25 @@ func TestServer_GRPC(t *testing.T) {
 //nolint:thelper
 func testServer(t *testing.T, disableGRPC bool) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	apiServerInfo := startServer(t, env, true)
+	apiServerInfo := servertesting.StartServer(t, env, true)
 
 	apiServerInfo.DisableGRPC = disableGRPC
 
 	ctx2, cancel := context.WithCancel(ctx)
 
-	rep, err := repo.OpenAPIServer(ctx2, apiServerInfo, repo.ClientOptions{
-		Username: testUsername,
-		Hostname: testHostname,
-	}, &content.CachingOptions{
+	rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, apiServerInfo, repo.ClientOptions{
+		Username: servertesting.TestUsername,
+		Hostname: servertesting.TestHostname,
+	}, content.CachingOptions{
 		CacheDirectory:    testutil.TempDirectory(t),
 		MaxCacheSizeBytes: maxCacheSizeBytes,
-	}, testPassword)
+	}, servertesting.TestPassword, &repo.Options{})
 
 	// cancel immediately to ensure we did not spawn goroutines that depend on ctx inside
 	// repo.OpenAPIServer()
 	cancel()
 
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	defer rep.Close(ctx)
 
@@ -135,12 +65,12 @@ func testServer(t *testing.T, disableGRPC bool) {
 
 func TestGRPCServer_AuthenticationError(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	apiServerInfo := startServer(t, env, true)
+	apiServerInfo := servertesting.StartServer(t, env, true)
 
-	if _, err := repo.OpenAPIServer(ctx, apiServerInfo, repo.ClientOptions{
+	if _, err := servertesting.ConnectAndOpenAPIServer(t, ctx, apiServerInfo, repo.ClientOptions{
 		Username: "bad-username",
 		Hostname: "bad-hostname",
-	}, nil, "bad-password"); err == nil {
+	}, content.CachingOptions{}, "bad-password", &repo.Options{}); err == nil {
 		t.Fatal("unexpected success when connecting with invalid username")
 	}
 }
@@ -148,21 +78,21 @@ func TestGRPCServer_AuthenticationError(t *testing.T) {
 //nolint:gocyclo
 func TestServerUIAccessDeniedToRemoteUser(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
-	si := startServer(t, env, true)
+	si := servertesting.StartServer(t, env, true)
 
 	remoteUserClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
-		Username:                            testUsername + "@" + testHostname,
-		Password:                            testPassword,
+		Username:                            servertesting.TestUsername + "@" + servertesting.TestHostname,
+		Password:                            servertesting.TestPassword,
 	})
 	require.NoError(t, err)
 
 	uiUserWithoutCSRFToken, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
@@ -170,8 +100,8 @@ func TestServerUIAccessDeniedToRemoteUser(t *testing.T) {
 	uiUserClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
 		BaseURL:                             si.BaseURL,
 		TrustedServerCertificateFingerprint: si.TrustedServerCertificateFingerprint,
-		Username:                            testUIUsername,
-		Password:                            testUIPassword,
+		Username:                            servertesting.TestUIUsername,
+		Password:                            servertesting.TestUIPassword,
 	})
 
 	require.NoError(t, err)
@@ -250,8 +180,8 @@ func remoteRepositoryTest(ctx context.Context, t *testing.T, rep repo.Repository
 		manifestID, manifestID2 manifest.ID
 		written                 = make([]byte, 100000)
 		srcInfo                 = snapshot.SourceInfo{
-			Host:     testHostname,
-			UserName: testUsername,
+			Host:     servertesting.TestHostname,
+			UserName: servertesting.TestUsername,
 			Path:     testPathname,
 		}
 	)
@@ -415,13 +345,11 @@ func mustListSnapshotCount(ctx context.Context, t *testing.T, rep repo.Repositor
 	t.Helper()
 
 	snaps, err := snapshot.ListSnapshots(ctx, rep, snapshot.SourceInfo{
-		UserName: testUsername,
-		Host:     testHostname,
+		UserName: servertesting.TestUsername,
+		Host:     servertesting.TestHostname,
 		Path:     testPathname,
 	})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	if got, want := len(snaps), wantCount; got != want {
 		t.Fatalf("unexpected number of snapshots: %v, want %v", got, want)
diff --git a/internal/servertesting/servertesting.go b/internal/servertesting/servertesting.go
new file mode 100644
index 00000000..b0b59633
--- /dev/null
+++ b/internal/servertesting/servertesting.go
@@ -0,0 +1,107 @@
+// Package servertesting provides helpers for launching and testing Kopia server.
+package servertesting
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"net/http/httptest"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/auth"
+	"github.com/kopia/kopia/internal/passwordpersist"
+	"github.com/kopia/kopia/internal/repotesting"
+	"github.com/kopia/kopia/internal/server"
+	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/testutil"
+	"github.com/kopia/kopia/repo"
+	"github.com/kopia/kopia/repo/content"
+)
+
+const (
+	TestUsername = "foo"
+	TestHostname = "bar"
+	TestPassword = "123"
+
+	TestUIUsername = "ui-user"
+	TestUIPassword = "123456"
+)
+
+// StartServer starts a test server and returns APIServerInfo.
+func StartServer(t *testing.T, env *repotesting.Environment, tls bool) *repo.APIServerInfo {
+	t.Helper()
+
+	ctx := testlogging.Context(t)
+
+	s, err := server.New(ctx, &server.Options{
+		ConfigFile:      env.ConfigFile(),
+		PasswordPersist: passwordpersist.File(),
+		Authorizer:      auth.LegacyAuthorizer(),
+		Authenticator: auth.CombineAuthenticators(
+			auth.AuthenticateSingleUser(TestUsername+"@"+TestHostname, TestPassword),
+			auth.AuthenticateSingleUser(TestUIUsername, TestUIPassword),
+		),
+		RefreshInterval:   1 * time.Minute,
+		UIUser:            TestUIUsername,
+		UIPreferencesFile: filepath.Join(testutil.TempDirectory(t), "ui-pref.json"),
+	})
+
+	require.NoError(t, err)
+
+	s.SetRepository(ctx, env.Repository)
+
+	// ensure we disconnect the repository before shutting down the server.
+	t.Cleanup(func() { s.SetRepository(ctx, nil) })
+
+	require.NoError(t, err)
+
+	asi := &repo.APIServerInfo{}
+
+	m := mux.NewRouter()
+	s.SetupHTMLUIAPIHandlers(m)
+	s.SetupRepositoryAPIHandlers(m)
+	s.SetupControlAPIHandlers(m)
+	s.ServeStaticFiles(m, server.AssetFile())
+
+	hs := httptest.NewUnstartedServer(s.GRPCRouterHandler(m))
+	if tls {
+		hs.EnableHTTP2 = true
+		hs.StartTLS()
+		serverHash := sha256.Sum256(hs.Certificate().Raw)
+		asi.BaseURL = hs.URL
+		asi.TrustedServerCertificateFingerprint = hex.EncodeToString(serverHash[:])
+	} else {
+		hs.Start()
+		asi.BaseURL = hs.URL
+	}
+
+	t.Cleanup(hs.Close)
+
+	return asi
+}
+
+// ConnectAndOpenAPIServer creates temporary config file and to and opens API server for testing.
+func ConnectAndOpenAPIServer(t *testing.T, ctx context.Context, asi *repo.APIServerInfo, rco repo.ClientOptions, caching content.CachingOptions, password string, opt *repo.Options) (repo.Repository, error) {
+	t.Helper()
+
+	configFile := filepath.Join(t.TempDir(), "tmp.config")
+
+	if err := repo.ConnectAPIServer(ctx, configFile, asi, password, &repo.ConnectOptions{
+		ClientOptions:  rco,
+		CachingOptions: caching,
+	}); err != nil {
+		return nil, err
+	}
+
+	t.Cleanup(func() {
+		repo.Disconnect(ctx, configFile)
+	})
+
+	//
+	return repo.Open(ctx, configFile, password, opt)
+}
diff --git a/internal/sparsefile/sparsefile_test.go b/internal/sparsefile/sparsefile_test.go
index 2096b8d7..2714749c 100644
--- a/internal/sparsefile/sparsefile_test.go
+++ b/internal/sparsefile/sparsefile_test.go
@@ -7,6 +7,8 @@
 	"runtime"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/kopia/kopia/internal/stat"
 )
 
@@ -20,9 +22,7 @@ func TestSparseCopy(t *testing.T) {
 	dir := t.TempDir()
 
 	blk, err := stat.GetBlockSize(dir)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	type chunk struct {
 		slice []byte
diff --git a/internal/stat/stat_test.go b/internal/stat/stat_test.go
index 4b10ca94..538de678 100644
--- a/internal/stat/stat_test.go
+++ b/internal/stat/stat_test.go
@@ -8,13 +8,13 @@
 	"os"
 	"path/filepath"
 	"testing"
+
+	"github.com/stretchr/testify/require"
 )
 
 func TestGetBlockSize(t *testing.T) {
 	s, err := GetBlockSize(os.DevNull)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	if s <= 0 {
 		t.Fatalf("invalid disk block size: %d, must be greater than 0", s)
@@ -29,9 +29,7 @@ func TestGetFileAllocSize(t *testing.T) {
 	data := bytes.Repeat([]byte{1}, size)
 
 	err := os.WriteFile(f, data, os.ModePerm)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	s, err := GetFileAllocSize(f)
 	if err != nil {
diff --git a/repo/blob/filesystem/filesystem_storage_test.go b/repo/blob/filesystem/filesystem_storage_test.go
index dea054f1..4601f4f1 100644
--- a/repo/blob/filesystem/filesystem_storage_test.go
+++ b/repo/blob/filesystem/filesystem_storage_test.go
@@ -139,9 +139,7 @@ func TestFileStorageConcurrency(t *testing.T) {
 	st, err := New(ctx, &Options{
 		Path: path,
 	}, true)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	blobtesting.VerifyConcurrentAccess(t, st, blobtesting.ConcurrentAccessOptions{
 		NumBlobs:                        16,
diff --git a/repo/blob/sftp/sftp_storage_test.go b/repo/blob/sftp/sftp_storage_test.go
index d6019f57..ed138f99 100644
--- a/repo/blob/sftp/sftp_storage_test.go
+++ b/repo/blob/sftp/sftp_storage_test.go
@@ -38,14 +38,10 @@ func mustGetLocalTmpDir(t *testing.T) string {
 	t.Helper()
 
 	tmpDir, err := os.MkdirTemp(".", ".creds")
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	tmpDir, err = filepath.Abs(tmpDir)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	t.Cleanup(func() {
 		os.RemoveAll(tmpDir)
@@ -80,9 +76,7 @@ func mustRunCommand(t *testing.T, cmd string, args ...string) []byte {
 	t.Helper()
 
 	v, err := runAndGetOutput(t, cmd, args...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	return v
 }
@@ -344,9 +338,7 @@ func mustReadFileToString(t *testing.T, fname string) string {
 	t.Helper()
 
 	data, err := os.ReadFile(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	return string(data)
 }
diff --git a/repo/content/sessions_test.go b/repo/content/sessions_test.go
index c6b76fcf..16c1da95 100644
--- a/repo/content/sessions_test.go
+++ b/repo/content/sessions_test.go
@@ -3,6 +3,8 @@
 import (
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/kopia/kopia/internal/clock"
 	"github.com/kopia/kopia/repo/blob"
 )
@@ -11,19 +13,13 @@ func TestGenerateSessionID(t *testing.T) {
 	n := clock.Now()
 
 	s1, err := generateSessionID(n)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	s2, err := generateSessionID(n)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	s3, err := generateSessionID(n)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	m := map[SessionID]bool{
 		s1: true,
diff --git a/repo/maintenance/blob_gc_test.go b/repo/maintenance/blob_gc_test.go
index 092b6967..e5ba92d5 100644
--- a/repo/maintenance/blob_gc_test.go
+++ b/repo/maintenance/blob_gc_test.go
@@ -55,9 +55,7 @@ func (s *formatSpecificTestSuite) TestDeleteUnreferencedBlobs(t *testing.T) {
 	env.RepositoryWriter.Flush(ctx)
 
 	blobsBefore, err := blob.ListAllBlobs(ctx, env.RepositoryWriter.BlobStorage(), "")
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	if got, want := len(blobsBefore), 4; got != want {
 		t.Fatalf("unexpected number of blobs after writing: %v", blobsBefore)
@@ -153,9 +151,7 @@ func (s *formatSpecificTestSuite) TestDeleteUnreferencedBlobs(t *testing.T) {
 	// make sure we're back to the starting point.
 
 	blobsAfter, err := blob.ListAllBlobs(ctx, env.RepositoryWriter.BlobStorage(), "")
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	if diff := cmp.Diff(blobsBefore, blobsAfter); diff != "" {
 		t.Fatalf("unexpected diff: %v", diff)
@@ -190,9 +186,7 @@ func mustPutDummySessionBlob(t *testing.T, st blob.Storage, sessionIDSuffix blob
 	t.Helper()
 
 	j, err := json.Marshal(si)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	h := hmac.New(sha256.New, testHMACSecret)
 	h.Write(j)
diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go
index b5d0fea8..43eedaf6 100644
--- a/repo/object/object_manager_test.go
+++ b/repo/object/object_manager_test.go
@@ -359,9 +359,7 @@ func verifyFull(ctx context.Context, t *testing.T, om *Manager, oid ID, want []b
 func verifyNoError(t *testing.T, err error) {
 	t.Helper()
 
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 }
 
 func verifyIndirectBlock(ctx context.Context, t *testing.T, om *Manager, oid ID) {
@@ -598,14 +596,10 @@ func mustWriteObject(t *testing.T, om *Manager, data []byte, compressor compress
 	defer w.Close()
 
 	_, err := w.Write(data)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	oid, err := w.Result()
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	return oid
 }
diff --git a/repo/repository_test.go b/repo/repository_test.go
index 40ca7f11..2fafac49 100644
--- a/repo/repository_test.go
+++ b/repo/repository_test.go
@@ -7,6 +7,7 @@
 	"math/rand"
 	"runtime/debug"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -16,8 +17,11 @@
 	"github.com/kopia/kopia/internal/cache"
 	"github.com/kopia/kopia/internal/epoch"
 	"github.com/kopia/kopia/internal/gather"
+	"github.com/kopia/kopia/internal/metricid"
 	"github.com/kopia/kopia/internal/repotesting"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
 	"github.com/kopia/kopia/repo/blob/beforeop"
@@ -583,8 +587,21 @@ func TestObjectWritesWithRetention(t *testing.T) {
 	}))
 }
 
-func (s *formatSpecificTestSuite) TestWriteSessionFlushOnSuccess(t *testing.T) {
-	ctx, env := repotesting.NewEnvironment(t, s.formatVersion)
+func TestWriteSessionFlushOnSuccess(t *testing.T) {
+	var beforeFlushCount, afterFlushCount atomic.Int32
+
+	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant, repotesting.Options{
+		OpenOptions: func(o *repo.Options) {
+			o.BeforeFlush = append(o.BeforeFlush, func(ctx context.Context, w repo.RepositoryWriter) error {
+				beforeFlushCount.Add(1)
+				w.OnSuccessfulFlush(func(ctx context.Context, w repo.RepositoryWriter) error {
+					afterFlushCount.Add(1)
+					return nil
+				})
+				return nil
+			})
+		},
+	})
 
 	var oid object.ID
 
@@ -593,7 +610,107 @@ func (s *formatSpecificTestSuite) TestWriteSessionFlushOnSuccess(t *testing.T) {
 		return nil
 	}))
 
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
 	verify(ctx, t, env.Repository, oid, []byte{1, 2, 3}, "test-1")
+
+	someErr := errors.Errorf("some error")
+
+	require.ErrorIs(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4}, "test-2")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	require.ErrorIs(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{
+		FlushOnFailure: true,
+	}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4, 5}, "test-3")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 2, beforeFlushCount.Load())
+	require.EqualValues(t, 2, afterFlushCount.Load())
+}
+
+func TestWriteSessionFlushOnSuccessClient_REST(t *testing.T) {
+	testWriteSessionFlushOnSuccessClient(t, true)
+}
+
+func TestWriteSessionFlushOnSuccessClient_GRPC(t *testing.T) {
+	testWriteSessionFlushOnSuccessClient(t, false)
+}
+
+//nolint:thelper
+func testWriteSessionFlushOnSuccessClient(t *testing.T, disableGRPC bool) {
+	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant, repotesting.Options{})
+
+	apiServerInfo := servertesting.StartServer(t, env, true)
+	apiServerInfo.DisableGRPC = disableGRPC
+
+	var beforeFlushCount, afterFlushCount atomic.Int32
+
+	ctx2, cancel := context.WithCancel(testlogging.Context(t))
+	defer cancel()
+
+	rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, apiServerInfo, repo.ClientOptions{
+		Username: servertesting.TestUsername,
+		Hostname: servertesting.TestHostname,
+	}, content.CachingOptions{
+		CacheDirectory: testutil.TempDirectory(t),
+	}, servertesting.TestPassword, &repo.Options{
+		BeforeFlush: []repo.RepositoryWriterCallback{
+			func(ctx context.Context, w repo.RepositoryWriter) error {
+				beforeFlushCount.Add(1)
+				w.OnSuccessfulFlush(func(ctx context.Context, w repo.RepositoryWriter) error {
+					afterFlushCount.Add(1)
+					return nil
+				})
+				return nil
+			},
+		},
+	})
+
+	defer rep.Close(ctx) //nolint:errcheck,staticcheck
+
+	require.NoError(t, err)
+
+	var oid object.ID
+
+	require.NoError(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3}, "test-1")
+		return nil
+	}))
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	verify(ctx, t, rep, oid, []byte{1, 2, 3}, "test-1")
+
+	someErr := errors.Errorf("some error")
+
+	require.ErrorIs(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4}, "test-2")
+		return someErr
+	}), someErr)
+
+	require.EqualValues(t, 1, beforeFlushCount.Load())
+	require.EqualValues(t, 1, afterFlushCount.Load())
+
+	require.ErrorIs(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{
+		FlushOnFailure: true,
+	}, func(ctx context.Context, w repo.RepositoryWriter) error {
+		oid = writeObject(ctx, t, w, []byte{1, 2, 3, 4, 5}, "test-3")
+		return someErr
+	}), someErr)
+
+	t.Logf("-----")
+
+	require.EqualValues(t, 2, beforeFlushCount.Load())
+	require.EqualValues(t, 2, afterFlushCount.Load())
 }
 
 func (s *formatSpecificTestSuite) TestWriteSessionNoFlushOnFailure(t *testing.T) {
@@ -713,6 +830,24 @@ func ensureMapEntry[T any](t *testing.T, m map[string]T, key string) T {
 	return actual
 }
 
+func TestAllRegistryMetricsAreMapped(t *testing.T) {
+	_, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
+
+	snap := env.Repository.Metrics().Snapshot(false)
+
+	for s := range snap.Counters {
+		require.Contains(t, metricid.Counters.NameToIndex, s)
+	}
+
+	for s := range snap.DurationDistributions {
+		require.Contains(t, metricid.DurationDistributions.NameToIndex, s)
+	}
+
+	for s := range snap.SizeDistributions {
+		require.Contains(t, metricid.SizeDistributions.NameToIndex, s)
+	}
+}
+
 func TestDeriveKey(t *testing.T) {
 	testPurpose := []byte{0, 0, 0, 0}
 	testKeyLength := 8
diff --git a/snapshot/snapshot_test.go b/snapshot/snapshot_test.go
index 633abfd0..77c22bae 100644
--- a/snapshot/snapshot_test.go
+++ b/snapshot/snapshot_test.go
@@ -200,9 +200,7 @@ func mustAbs(t *testing.T, p string) string {
 	t.Helper()
 
 	p2, err := filepath.Abs(p)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	return p2
 }
diff --git a/snapshot/snapshotmaintenance/snapshotmaintenance_test.go b/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
index a03155ce..a52c258d 100644
--- a/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
+++ b/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
@@ -337,9 +337,7 @@ func (th *testHarness) openAnother(t *testing.T) repo.RepositoryWriter {
 	})
 
 	_, w, err := r.NewWriter(ctx, repo.WriteSessionOptions{Purpose: "test"})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	return w
 }
diff --git a/tests/end_to_end_test/api_server_repository_test.go b/tests/end_to_end_test/api_server_repository_test.go
index 53c64c00..85529aa1 100644
--- a/tests/end_to_end_test/api_server_repository_test.go
+++ b/tests/end_to_end_test/api_server_repository_test.go
@@ -7,13 +7,17 @@
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/kopia/kopia/internal/apiclient"
 	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/servertesting"
 	"github.com/kopia/kopia/internal/testlogging"
 	"github.com/kopia/kopia/internal/testutil"
 	"github.com/kopia/kopia/internal/timetrack"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/blob"
+	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/tests/clitestutil"
 	"github.com/kopia/kopia/tests/testdirtree"
 	"github.com/kopia/kopia/tests/testenv"
@@ -124,29 +128,25 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 
 	// open repository client.
 	ctx2, cancel := context.WithCancel(ctx)
-	rep, err := repo.OpenAPIServer(ctx2, &repo.APIServerInfo{
+	rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, &repo.APIServerInfo{
 		BaseURL:                             sp.BaseURL,
 		TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
 		DisableGRPC:                         !useGRPC,
 	}, repo.ClientOptions{
 		Username: "foo",
 		Hostname: "bar",
-	}, nil, "baz")
+	}, content.CachingOptions{}, "baz", &repo.Options{})
 
 	// cancel immediately to ensure we did not spawn goroutines that depend on ctx inside
 	// repo.OpenAPIServer()
 	cancel()
 
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	// open new write session in repository client
 
 	_, writeSess, err := rep.NewWriter(ctx, repo.WriteSessionOptions{Purpose: "some writer"})
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	logErrorAndIgnore(t, serverapi.Shutdown(ctx, controlClient))
 
@@ -257,14 +257,14 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 	// open repository client to a dead server, this should fail quickly instead of retrying forever.
 	timer := timetrack.StartTimer()
 
-	repo.OpenAPIServer(ctx, &repo.APIServerInfo{
+	servertesting.ConnectAndOpenAPIServer(t, ctx, &repo.APIServerInfo{
 		BaseURL:                             sp.BaseURL,
 		TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
 		DisableGRPC:                         !useGRPC,
 	}, repo.ClientOptions{
 		Username: "foo",
 		Hostname: "bar",
-	}, nil, "baz")
+	}, content.CachingOptions{}, "baz", &repo.Options{})
 
 	//nolint:forbidigo
 	if dur := timer.Elapsed(); dur > 15*time.Second {
diff --git a/tests/end_to_end_test/restore_test.go b/tests/end_to_end_test/restore_test.go
index c4c00eed..2e5f5781 100644
--- a/tests/end_to_end_test/restore_test.go
+++ b/tests/end_to_end_test/restore_test.go
@@ -182,9 +182,7 @@ func TestSnapshotRestore(t *testing.T) {
 
 	// create a file with well-known name.
 	f, err := os.Create(filepath.Join(source, "single-file"))
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	fmt.Fprintf(f, "some-data")
 	f.Close()
@@ -427,9 +425,7 @@ func TestRestoreSnapshotOfSingleFile(t *testing.T) {
 	sourceFile := filepath.Join(sourceDir, "single-file")
 
 	f, err := os.Create(sourceFile)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	fmt.Fprintf(f, "some-data")
 	f.Close()
@@ -765,9 +761,7 @@ func verifyFileMode(t *testing.T, filename string, want os.FileMode) {
 	}
 
 	s, err := os.Lstat(filename)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	// make sure we restored permissions correctly
 	if s.Mode() != want {
@@ -779,9 +773,7 @@ func verifyValidZipFile(t *testing.T, fname string) {
 	t.Helper()
 
 	zr, err := zip.OpenReader(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	defer zr.Close()
 }
@@ -790,9 +782,8 @@ func verifyValidTarFile(t *testing.T, fname string) {
 	t.Helper()
 
 	f, err := os.Open(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
+
 	defer f.Close()
 
 	verifyValidTarReader(t, tar.NewReader(f))
@@ -815,15 +806,12 @@ func verifyValidTarGzipFile(t *testing.T, fname string) {
 	t.Helper()
 
 	f, err := os.Open(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
+
 	defer f.Close()
 
 	gz, err := gzip.NewReader(f)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	verifyValidTarReader(t, tar.NewReader(gz))
 }
@@ -842,9 +830,7 @@ func TestSnapshotRestoreByPath(t *testing.T) {
 
 	// create a file with well-known name.
 	f, err := os.Create(filepath.Join(source, "single-file"))
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	fmt.Fprintf(f, "some-data")
 	f.Close()
diff --git a/tests/end_to_end_test/snapshot_actions_test.go b/tests/end_to_end_test/snapshot_actions_test.go
index 570b489e..90642b78 100644
--- a/tests/end_to_end_test/snapshot_actions_test.go
+++ b/tests/end_to_end_test/snapshot_actions_test.go
@@ -368,17 +368,13 @@ func verifyFileExists(t *testing.T, fname string) {
 	t.Helper()
 
 	_, err := os.Stat(fname)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 }
 
 func verifyNoError(t *testing.T, err error) {
 	t.Helper()
 
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 }
 
 func mustReadEnvFile(t *testing.T, fname string) map[string]string {