packages feed

ephemeral-pg 0.2.1.0 → 0.2.2.0

raw patch · 5 files changed

+182/−9 lines, 5 filesdep ~bytestringdep ~directorydep ~filepathPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: bytestring, directory, filepath, hashable, network, process, temporary, transformers, typed-process, unix

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,14 @@ # Changelog +## 0.2.2.0++### Bug Fixes++- Make initdb cache creation atomic by copying into a same-parent temporary+  directory and publishing it with an atomic rename. Concurrent cold-cache+  writers now treat an existing final cache as success instead of exposing a+  partially-copied `data` directory.+ ## 0.2.1.0  ### Bug Fixes
README.md view
@@ -158,6 +158,16 @@   pure () ``` +### Suite-level fixtures with template databases++For larger integration suites where many examples need the same migrated+schema, consider starting one cached PostgreSQL server for the whole suite,+migrating a template database once, and cloning clean per-example databases+with PostgreSQL's `CREATE DATABASE ... TEMPLATE ...`.++See [Suite-level template databases](docs/suite-template-databases.md) for the+full pattern, tradeoffs, and a complete fixture implementation.+ ### Manual Lifecycle Management  For more control, use `start` and `stop` directly:@@ -430,6 +440,61 @@  Ensure you have write access to the temporary directory. By default, the system temp directory is used.++## Tracing++OpenTelemetry tracing is available through the optional companion+package `ephemeral-pg-opentelemetry`, which lives next to this library+in the same repository. It mirrors the `EphemeralPg` lifecycle API and+emits one span per public operation:++| Wrapper                      | Span name                       |+| ---------------------------- | ------------------------------- |+| `withTraced`                 | `ephemeralpg.with`              |+| `withCachedTraced`           | `ephemeralpg.with_cached`       |+| `startTraced`                | `ephemeralpg.start`             |+| `stopTraced`                 | `ephemeralpg.stop`              |+| `restartTraced`              | `ephemeralpg.restart`           |+| `createSnapshotTraced`       | `ephemeralpg.snapshot.create`   |+| `restoreSnapshotTraced`      | `ephemeralpg.snapshot.restore`  |+| `deleteSnapshotTraced`       | `ephemeralpg.snapshot.delete`   |+| `dumpTraced`                 | `ephemeralpg.dump`              |+| `restoreTraced`              | `ephemeralpg.restore`           |++Each span carries the standard database attributes+(`db.system.name`/`db.system`, `db.namespace`/`db.name`) plus+library-specific ones (`ephemeralpg.port`,+`ephemeralpg.shutdown.mode`). Attribute name selection obeys+`OTEL_SEMCONV_STABILITY_OPT_IN` exactly the way upstream HTTP+instrumentation does — set it to `http` for stable names, `http/dup`+for both stable and legacy. Errors from `EphemeralPg.start` are+recorded uniformly with `error.type` (constructor name), span status+`Error`, and a `recordException` event.++The wrappers are designed to nest under a parent test span. Combined+with `hs-opentelemetry-instrumentation-hspec`, a test that calls+`withTraced` produces a tree of the form:++    Run tests+      └─ ephemeral-pg under OpenTelemetry+         └─ emits a span tree under withTraced+            └─ ephemeralpg.with+               ├─ ephemeralpg.start+               ├─ ephemeralpg.with.body+               └─ ephemeralpg.stop++Add the dependency to your test-suite (the package is not yet on+Hackage; reference it via the local `cabal.project`):++```cabal+build-depends:+  ephemeral-pg-opentelemetry,+  hs-opentelemetry-api,+  hs-opentelemetry-sdk,+  hs-opentelemetry-instrumentation-hspec,+```++See `ephemeral-pg-opentelemetry/test/Demo.hs` for a runnable example.  ## License 
ephemeral-pg.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ephemeral-pg-version: 0.2.1.0+version: 0.2.2.0 synopsis: Temporary PostgreSQL databases for testing description:   A modern library for creating temporary PostgreSQL instances for testing.@@ -84,18 +84,52 @@   import: warnings   default-language: GHC2021   type: exitcode-stdio-1.0-  hs-source-dirs: test+  hs-source-dirs:+    test+    src+   main-is: Main.hs+  other-modules:+    EphemeralPg+    EphemeralPg.Config+    EphemeralPg.Database+    EphemeralPg.Error+    EphemeralPg.Internal.Cache+    EphemeralPg.Internal.CopyOnWrite+    EphemeralPg.Internal.Directory+    EphemeralPg.Internal.Except+    EphemeralPg.Internal.Port+    EphemeralPg.Process+    EphemeralPg.Process.CreateDb+    EphemeralPg.Process.InitDb+    EphemeralPg.Process.Postgres+   build-depends:     QuickCheck >=2.14 && <2.16,     base,+    bytestring,+    directory,     ephemeral-pg,+    filepath,+    hashable,     hasql,     hspec >=2.11 && <2.12,+    network,+    process,+    temporary,     text,+    transformers,+    typed-process,+    unix,    default-extensions:     BlockArguments+    DeriveAnyClass+    DerivingStrategies     DuplicateRecordFields+    LambdaCase+    NoFieldSelectors     OverloadedRecordDot     OverloadedStrings+    RecordWildCards+    StrictData
src/EphemeralPg/Internal/Cache.hs view
@@ -42,6 +42,7 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T+import Data.Unique (hashUnique, newUnique) import EphemeralPg.Config (Config (..)) import EphemeralPg.Internal.CopyOnWrite   ( CowCapability (..),@@ -57,9 +58,11 @@     listDirectory,     removeDirectoryRecursive,     removeFile,+    renamePath,   ) import System.Exit (ExitCode (..)) import System.FilePath ((</>))+import System.Posix.Process (getProcessID) import System.Process.Typed (byteStringOutput, proc, readProcess, setStderr, setStdout)  -- | A cache key uniquely identifies a cached initdb cluster.@@ -170,16 +173,42 @@  -- | Create a cache from an initialized data directory. createCache :: CacheKey -> FilePath -> Maybe FilePath -> IO (Either Text ())-createCache key srcDataDir mRoot = do-  dir <- getCacheDirectory key mRoot-  createDirectoryIfMissing True dir+createCache key srcDataDir mRoot = runExceptT $ do+  dir <- liftIO $ getCacheDirectory key mRoot+  liftIO $ createDirectoryIfMissing True dir   let dstDataDir = dir </> "data" -  -- Detect CoW capability-  cowCapability <- detectCowCapability dir+  alreadyCached <- liftIO $ doesDirectoryExist dstDataDir+  unless alreadyCached $ do+    unique <- liftIO newUnique+    pid <- liftIO getProcessID+    let tmpDataDir =+          dir </> ("data.tmp-" <> show pid <> "-" <> show (hashUnique unique)) -  -- Copy the data directory to the cache-  copyDirectory cowCapability srcDataDir dstDataDir+    cowCapability <- liftIO $ detectCowCapability dir+    copyResult <- liftIO $ copyDirectory cowCapability srcDataDir tmpDataDir+    case copyResult of+      Left err -> do+        liftIO $ removeDirectoryIfExists tmpDataDir+        throwE err+      Right () -> publishCache tmpDataDir dstDataDir+  where+    publishCache :: FilePath -> FilePath -> ExceptT Text IO ()+    publishCache tmpDataDir dstDataDir = do+      result <- liftIO $ try @SomeException $ renamePath tmpDataDir dstDataDir+      case result of+        Right () -> pure ()+        Left ex -> do+          winnerExists <- liftIO $ doesDirectoryExist dstDataDir+          liftIO $ removeDirectoryIfExists tmpDataDir+          unless winnerExists $+            throwE $+              "Failed to publish cache: " <> T.pack (show ex)++    removeDirectoryIfExists :: FilePath -> IO ()+    removeDirectoryIfExists path = do+      exists <- doesDirectoryExist path+      when exists $ removeDirectoryRecursive path  -- | Restore from cache to a new data directory. restoreFromCache :: CacheKey -> FilePath -> Maybe FilePath -> IO (Either Text ())
test/Main.hs view
@@ -1,9 +1,20 @@ module Main where +import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)+import Control.Monad (forM_, replicateM) import Data.Text qualified as T import EphemeralPg qualified as Pg import EphemeralPg.Config qualified as Config+import EphemeralPg.Internal.Cache+  ( CacheKey (..),+    createCache,+    getCacheDirectory,+    restoreFromCache,+  ) import Hasql.Connection qualified as Connection+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist)+import System.FilePath ((</>))+import System.IO.Temp (withSystemTempDirectory) import Test.Hspec import Test.QuickCheck @@ -74,6 +85,31 @@             Connection.release conn             pure ()       result2 `shouldSatisfy` isRight++    it "publishes concurrently-created caches atomically" $ do+      withSystemTempDirectory "ephemeral-pg-cache-race-" $ \root -> do+        let key = CacheKey {pgVersion = "test", configHash = "atomic"}+            sourceDir = root </> "source-data"+            restoreDir = root </> "restore-data"++        createDirectoryIfMissing True (sourceDir </> "base")+        writeFile (sourceDir </> "PG_VERSION") "test\n"+        writeFile (sourceDir </> "base" </> "marker") "ok\n"++        done <- replicateM 8 newEmptyMVar+        forM_ done $ \var ->+          forkIO (createCache key sourceDir (Just root) >>= putMVar var)+        results <- traverse takeMVar done+        results `shouldSatisfy` all isRight++        cacheDir <- getCacheDirectory key (Just root)+        doesFileExist (cacheDir </> "data" </> "PG_VERSION") `shouldReturn` True+        doesFileExist (cacheDir </> "data" </> "base" </> "marker") `shouldReturn` True+        doesDirectoryExist (cacheDir </> "data" </> "source-data") `shouldReturn` False++        restoreResult <- restoreFromCache key restoreDir (Just root)+        restoreResult `shouldSatisfy` isRight+        doesFileExist (restoreDir </> "PG_VERSION") `shouldReturn` True    describe "Config" $ do     it "satisfies left identity (mempty <> x = x)" $