Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- uses: haskell-actions/hlint-run@v2
with:
path: '["src/", "test/", "effectful-data-cache-opentelemetry/src/", "effectful-data-cache-opentelemetry/test/"]'
path: '["src/", "test/", "effectful-data-cache-opentelemetry/src/", "effectful-data-cache-opentelemetry/test/", "effectful-data-cache-memcache/src/", "effectful-data-cache-memcache/test/"]'
fail-on: warning

format:
Expand All @@ -39,6 +39,8 @@ jobs:
test/**/*.hs
effectful-data-cache-opentelemetry/src/**/*.hs
effectful-data-cache-opentelemetry/test/**/*.hs
effectful-data-cache-memcache/src/**/*.hs
effectful-data-cache-memcache/test/**/*.hs
build:
needs: generate-matrix
Expand All @@ -51,6 +53,10 @@ jobs:
steps:
- uses: actions/checkout@v6

# macOS runners have no service containers; memcache tests skip there
- if: runner.os == 'Linux'
run: docker run -d -p 11211:11211 memcached:1.6

- uses: freckle/stack-action@v5
with:
cache-prefix: v2/
Expand Down
20 changes: 19 additions & 1 deletion .releaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@
"haddockOutputDir": "effectful-data-cache-opentelemetry/.stack-work"
}
],
[
"semantic-release-haddock-since",
{
"versionFiles": [
"effectful-data-cache-memcache/package.yaml",
"effectful-data-cache-memcache/*.cabal"
],
"baseline": "effectful-data-cache-memcache/api/effectful-data-cache-memcache.api",
"srcDir": "effectful-data-cache-memcache/src",
"haddockCommand": ["stack", "haddock", "--no-haddock-deps", "effectful-data-cache-memcache"],
"buildCommand": ["stack", "build", "effectful-data-cache-memcache"],
"haddockOutputDir": "effectful-data-cache-memcache/.stack-work"
}
],
[
"@semantic-release/git",
{
Expand All @@ -69,7 +83,11 @@
"effectful-data-cache-opentelemetry/package.yaml",
"effectful-data-cache-opentelemetry/*.cabal",
"effectful-data-cache-opentelemetry/src/**/*.hs",
"effectful-data-cache-opentelemetry/api/*.api"
"effectful-data-cache-opentelemetry/api/*.api",
"effectful-data-cache-memcache/package.yaml",
"effectful-data-cache-memcache/*.cabal",
"effectful-data-cache-memcache/src/**/*.hs",
"effectful-data-cache-memcache/api/*.api"
]
}
],
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Haskell library (stack + hpack). Edit `package.yaml`, never generated `.cabal`.
## Commands

- build and test (GHC 9.10): `stack build --test --ghc-options=-Werror`
- format: `fourmolu -i src test effectful-data-cache-opentelemetry/{src,test}` (fourmolu 0.21.0.0, config in `fourmolu.yaml`)
- lint: `hlint src test effectful-data-cache-opentelemetry/{src,test}` (rules in `.hlint.yaml`; HLS has no hlint plugin on GHC 9.10)
- format: `fourmolu -i src test effectful-data-cache-{opentelemetry,memcache}/{src,test}` (fourmolu 0.21.0.0, config in `fourmolu.yaml`)
- lint: `hlint src test effectful-data-cache-{opentelemetry,memcache}/{src,test}` (rules in `.hlint.yaml`; HLS has no hlint plugin on GHC 9.10)

CI (`.github/workflows/stack.yml`) runs all of the above; keep it green.

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ main = runEff . runCache Nothing $ do

See [effectful-data-cache-opentelemetry](effectful-data-cache-opentelemetry)
for OpenTelemetry instrumentation.

## memcached

See [effectful-data-cache-memcache](effectful-data-cache-memcache) for a
memcached backend.
1 change: 1 addition & 0 deletions effectful-data-cache-memcache/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
26 changes: 26 additions & 0 deletions effectful-data-cache-memcache/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Copyright 2026 drlkf

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20 changes: 20 additions & 0 deletions effectful-data-cache-memcache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# effectful-data-cache-memcache

`runCacheMemcache` interprets `Cache` against memcached using the
[memcache](https://hackage.haskell.org/package/memcache) client.

```haskell
import Database.Memcache.Client (def, newClient)
import Effectful.Cache.Memcache

codec :: Codec String Int
codec = Codec BC.pack (BC.pack . show) (readMaybe . BC.unpack)

main = do
c <- newClient [def] def
runEff . runCacheMemcache codec c Nothing $ insert "key" (42 :: Int)
```

memcached cannot enumerate items: `keys`, `filterWithKey` and `size` throw
`MemcacheUnsupported`. `lookup'` behaves like `lookup` and `purgeExpired` is
a no-op. Expirations are rounded up to whole seconds.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Effectful.Cache.Memcache.Codec
Effectful.Cache.Memcache.MemcacheUnsupported
Effectful.Cache.Memcache.runCacheMemcache
70 changes: 70 additions & 0 deletions effectful-data-cache-memcache/effectful-data-cache-memcache.cabal
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
cabal-version: 2.2

-- This file has been generated from package.yaml by hpack version 0.39.6.
--
-- see: https://github.com/sol/hpack

name: effectful-data-cache-memcache
version: 0.1.0.0
synopsis: memcached backend for effectful-data-cache
description: Please see the README on GitHub at <https://github.com/haskell-github-trust/effectful-data-cache/tree/main/effectful-data-cache-memcache#readme>
category: cache
homepage: https://github.com/haskell-github-trust/effectful-data-cache#readme
bug-reports: https://github.com/haskell-github-trust/effectful-data-cache/issues
author: drlkf
maintainer: drlkf <drlkf@drlkf.net>
copyright: 2026 drlkf
license: BSD-3-Clause
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
extra-doc-files:
CHANGELOG.md

source-repository head
type: git
location: https://github.com/haskell-github-trust/effectful-data-cache

library
exposed-modules:
Effectful.Cache.Memcache
other-modules:
Paths_effectful_data_cache_memcache
autogen-modules:
Paths_effectful_data_cache_memcache
hs-source-dirs:
src
ghc-options: -Wall -Wextra -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
build-depends:
base >=4.7 && <5
, bytestring >=0.11 && <0.13
, clock ==0.8.*
, effectful-core >=2.3 && <3
, effectful-data-cache ==0.2.*
, memcache ==0.3.*
default-language: Haskell2010

test-suite effectful-data-cache-memcache-test
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Effectful.Cache.MemcacheSpec
Paths_effectful_data_cache_memcache
autogen-modules:
Paths_effectful_data_cache_memcache
hs-source-dirs:
test
ghc-options: -Wall -Wextra -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
build-tool-depends:
hspec-discover:hspec-discover
build-depends:
base >=4.7 && <5
, bytestring
, clock
, effectful-core
, effectful-data-cache
, effectful-data-cache-memcache
, hspec
, memcache
default-language: Haskell2010
62 changes: 62 additions & 0 deletions effectful-data-cache-memcache/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: effectful-data-cache-memcache
version: 0.1.0.0
github: haskell-github-trust/effectful-data-cache
license: BSD-3-Clause
author: drlkf
maintainer: drlkf <drlkf@drlkf.net>
copyright: 2026 drlkf

extra-source-files:
- README.md

extra-doc-files:
- CHANGELOG.md

category: cache
synopsis: memcached backend for effectful-data-cache
description: >-
Please see the README on GitHub at
<https://github.com/haskell-github-trust/effectful-data-cache/tree/main/effectful-data-cache-memcache#readme>
dependencies:
- base >= 4.7 && < 5
- bytestring >= 0.11 && < 0.13
- clock >= 0.8 && < 0.9
- effectful-core >= 2.3 && < 3
- effectful-data-cache >= 0.2 && < 0.3
- memcache >= 0.3 && < 0.4

ghc-options:
- -Wall
- -Wextra
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints

library:
source-dirs: src

tests:
effectful-data-cache-memcache-test:
main: Spec.hs
source-dirs: test
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- effectful-data-cache-memcache
- effectful-data-cache
- effectful-core
- bytestring
- clock
- hspec
- memcache
build-tools:
- hspec-discover
105 changes: 105 additions & 0 deletions effectful-data-cache-memcache/src/Effectful/Cache/Memcache.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

-- | Interpret 'Cache' against a memcached server via "Database.Memcache.Client".
--
-- > c <- newClient [def] def
-- > runEff . runCacheMemcache codec c Nothing $ insert "a" (1 :: Int)
--
-- memcached cannot enumerate its items, so 'Keys', 'FilterWithKey' and 'Size'
-- throw 'MemcacheUnsupported'. Expired items are never returned, so 'Lookup''
-- behaves like 'Lookup', and 'PurgeExpired' is a no-op.
module Effectful.Cache.Memcache (
Codec (..),
MemcacheUnsupported (..),
runCacheMemcache,
) where

import Control.Exception (Exception, throwIO)
import Control.Monad (void)
import Data.ByteString (ByteString)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Word (Word32)
import qualified Database.Memcache.Client as M
import Effectful (Eff, IOE, MonadIO (liftIO), type (:>))
import Effectful.Cache (Cache (..))
import Effectful.Dispatch.Dynamic (interpret)
import System.Clock (Clock (Realtime), TimeSpec (..), getTime, toNanoSecs)

-- | How keys and values are stored. A value that fails to decode is a miss.
--
-- @since 0.1.0.0
data Codec k v = Codec
{ encodeKey :: k -> ByteString
, encodeValue :: v -> ByteString
, decodeValue :: ByteString -> Maybe v
}

-- | Thrown by operations memcached has no equivalent for.
--
-- @since 0.1.0.0
newtype MemcacheUnsupported = MemcacheUnsupported String
deriving Show

instance Exception MemcacheUnsupported

-- | Run against a memcached client with the given default expiration.
--
-- @since 0.1.0.0
runCacheMemcache
:: forall k v es a
. IOE :> es
=> Codec k v
-> M.Client
-> Maybe TimeSpec
-> Eff (Cache k v : es) a
-> Eff es a
runCacheMemcache codec c ts eff = do
ref <- liftIO (newIORef ts)
interpret (\_ op -> liftIO (withMemcache codec c ref op)) eff

withMemcache
:: Codec k v
-> M.Client
-> IORef (Maybe TimeSpec)
-> Cache k v m a
-> IO a
withMemcache codec c ref = \case
Insert k v -> readIORef ref >>= \ts -> set ts k v
Insert' ts k v -> set ts k v
Lookup k -> get k
Lookup' k -> get k
Keys -> unsupported "keys"
Delete k -> del k
FilterWithKey _ -> unsupported "filterWithKey"
Purge -> M.flush c Nothing
PurgeExpired -> pure ()
Size -> unsupported "size"
DefaultExpiration -> readIORef ref
SetDefaultExpiration ts -> writeIORef ref ts
where
key = encodeKey codec
get k = (>>= \(v, _, _) -> decodeValue codec v) <$> M.get c (key k)
del k = void (M.delete c (key k) 0)
set ts k v =
expiration ts >>= \case
Nothing -> del k
Just e -> void (M.set c (key k) (encodeValue codec v) 0 e)
unsupported op = throwIO (MemcacheUnsupported op)

-- | memcached expirations are whole seconds, 0 never expires, and values over
-- 30 days are unix timestamps. 'Nothing' means already expired.
expiration :: Maybe TimeSpec -> IO (Maybe Word32)
expiration Nothing = pure (Just 0)
expiration (Just ts)
| secs <= 0 = pure Nothing
| secs <= thirtyDays = pure (Just (fromInteger secs))
| otherwise = Just . fromInteger . (+ secs) . toInteger . sec <$> getTime Realtime
where
secs = negate (toNanoSecs ts `div` negate 1_000_000_000)
thirtyDays = 30 * 24 * 3600
Loading
Loading