Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
346d4f0
.gitignore for .elixir_ls
cjbottaro Nov 9, 2020
0a40c93
Starting over... again
cjbottaro Nov 10, 2020
87aa469
Start multi APIs
cjbottaro Nov 19, 2020
c1458cb
Basic connection
cjbottaro Jun 8, 2021
c88ce71
Progress
cjbottaro Jun 13, 2021
5769ba7
Simplify Connection API, coders, docs
cjbottaro Jul 3, 2021
b0649db
Polish up coders, tests, and docs
cjbottaro Jul 5, 2021
e935044
Basic cluster support, Ruby compat tests
cjbottaro Jul 5, 2021
3626f86
Client, tests, docs
cjbottaro Jul 24, 2021
e079135
Delete command, more API for client/cluster
cjbottaro Jul 24, 2021
176bdf0
Typo
cjbottaro Jul 24, 2021
85cfea1
Words
cjbottaro Jul 24, 2021
37048d8
Doc stuff
cjbottaro Jul 24, 2021
ec1ccee
Changelog
cjbottaro Jul 24, 2021
88b7cc3
Use errors, fix connection_test
cjbottaro Aug 12, 2022
77975e0
All tests passing
cjbottaro Aug 13, 2022
34a49db
Simplify startup
cjbottaro Aug 13, 2022
a93c4c5
Better docker compose file
cjbottaro Aug 13, 2022
1d50da6
Use Cream.Item pervasively
cjbottaro Aug 14, 2022
81a5c5b
Specs are good
cjbottaro Aug 14, 2022
2023b38
Fix flush
cjbottaro Aug 14, 2022
ec32a6e
Read through cache
cjbottaro Aug 14, 2022
da1922c
More docs
cjbottaro Aug 14, 2022
fc317b8
Clean up logging
cjbottaro Aug 14, 2022
1cf11b4
Docs, specs, config
cjbottaro Mar 25, 2023
0fab814
Fix tests
cjbottaro Mar 25, 2023
3a060d2
Bump dalli
cjbottaro Mar 25, 2023
71697aa
Merge branch 'master' into 1.0
cjbottaro Mar 25, 2023
f9de278
Enhancements for Superbolide
cjbottaro Apr 23, 2023
7cb0944
Merge branch '1.0' of https://github.com/cjbottaro/cream_ex into 1.0
cjbottaro Apr 23, 2023
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ erl_crash.dump
*.ez

.bundle

.elixir_ls

vendor/bundle
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 2.7.6
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# cream changes

## 1.0.0
-----------
* Simplified API; no more multi-get or multi-set (subject to change if need arises).
* Lazy connection pooling via `NimblePool`.
* Increase compatibility with Dalli via serialization that is aware of flags.
* Use `telemetry` for instrumentation.
* Drop dependency on `memcachex`.

## 0.2.0
-----------
* Integrate with `Instrumentation` package
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
dalli (3.2.3)
dalli (3.2.4)

PLATFORMS
ruby
Expand All @@ -10,4 +10,4 @@ DEPENDENCIES
dalli

BUNDLED WITH
1.15.1
2.1.4
236 changes: 25 additions & 211 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,231 +2,43 @@

A Dalli compatible memcached client.

It uses the same consistent hashing algorithm to connect to a cluster of memcached servers.

## Table of contents

1. [Features](#features)
1. [Installation](#installation)
1. [Quickstart](#quickstart)
1. [Connecting to a cluster](#connecting-to-a-cluster)
1. [Using modules](#using-modules)
1. [Memcachex options](#memcachex-options)
1. [Memcachex API](#memcachex-api)
1. [Ruby compatibility](#ruby-compatibility)
1. [Supervision](#supervision)
1. [Instrumentation](#instrumentation)
1. [Documentation](https://hexdocs.pm/cream/Cream.Cluster.html)
1. [Running the tests](#running-the-tests)
1. [TODO](#todo)

## Features

* connect to a "cluster" of memcached servers
* compatible with Ruby's Dallie gem (same consistent hashing algorithm)
* fetch with anonymous function
* multi set
* multi get
* multi fetch
* built in pooling via [poolboy](https://github.com/devinus/poolboy)
* complete supervision trees
* [fully documented](https://hexdocs.pm/cream/Cream.Cluster.html)
* instrumentation with the [Instrumentation](https://hexdocs.pm/instrumentation) package.


## Installation

In your `mix.exs` file...

```elixir
def deps do
[
{:cream, ">= 0.1.0"}
]
end
```
It uses the same consistent hashing algorithm as Dalli to determine which server
a key is on.

## Quickstart

```elixir
# Connects to localhost:11211 with worker pool of size 10
{:ok, cluster} = Cream.Cluster.start_link

# Single set and get
Cream.Cluster.set(cluster, {"name", "Callie"})
Cream.Cluster.get(cluster, "name")
# => "Callie"

# Single fetch
Cream.Cluster.fetch cluster, "some", fn ->
"thing"
end
# => "thing"

# Multi set / multi get with list
Cream.Cluster.set(cluster, [{"name", "Callie"}, {"buddy", "Chris"}])
Cream.Cluster.get(cluster, ["name", "buddy"])
# => %{"name" => "Callie", "buddy" => "Chris"}

# Multi set / multi get with map
Cream.Cluster.set(cluster, %{"species" => "canine", "gender" => "female"})
Cream.Cluster.get(cluster, ["species", "gender"])
# => %{"species" => "canine", "gender" => "female"}

# Multi fetch
Cream.Cluster.fetch cluster, ["foo", "bar", "baz"], fn missing_keys ->
Enum.map(missing_keys, &String.reverse/1)
end
# => %{"foo" => "oof", "bar" => "rab", "baz" => "zab"}
```

## Connecting to a cluster

```elixir
{:ok, cluster} = Cream.Cluster.start_link servers: ["cache01:11211", "cache02:11211"]
```

## Using modules

You can use modules to configure clusters, exactly like how Ecto repos work.

```elixir
# In config/*.exs

config :my_app, MyCluster,
servers: ["cache01:11211", "cache02:11211"],
pool: 5

# Elsewhere

defmodule MyCluster do
use Cream.Cluster, otp_app: :my_app

# Optional callback to do runtime configuration.
def init(config) do
# config = Keyword.put(config, :pool, System.get_env("POOL_SIZE"))
{:ok, config}
end
end

MyCluster.start_link
MyCluster.get("foo")
```

## Memcachex options

Cream uses Memcachex for individual connections to the cluster. You can pass
options to Memcachex via `Cream.Cluster.start_link/1`:
Sensible defaults...

```elixir
Cream.Cluster.start_link(
servers: ["localhost:11211"],
memcachex: [ttl: 3600, namespace: "foo"]
)
```
iex(1)> {:ok, client} = Cream.Client.start_link()
{:ok, #PID<0.265.0>}

Or if using modules:
iex(1)> Cream.Client.set(client, {"foo", "bar"})
:ok

```elixir
use Mix.Config

config :my_app, MyCluster,
servers: ["localhost:11211"],
memcachex: [ttl: 3600, namespace: "foo"]

MyCluster.start_link
iex(1)> Cream.Client.get(client, "foo")
{:ok, "bar"}
```

Any option you can pass to
[`Memcache.start_link`](https://hexdocs.pm/memcachex/Memcache.html#start_link/2),
you can pass via the `:memcachex` option for `Cream.Cluster.start_link`.

## Memcachex API

`Cream.Cluster`'s API is very small: `get`, `set`, `fetch`, `flush`. It may
expand in the future, but for now, you can access Memcachex's API directly
if you need.
As a module with custom config...

Cream will still provide worker pooling and key routing, even when using
Memcachex's API directly.

If you are using a single key, things are pretty straight forward...

```elixir
results = Cream.Cluster.with_conn cluster, key, fn conn ->
Memcache.get(conn, key)
end
```
import Config

It gets a bit more complex with a list of keys...
config MyClient, servers: ["memcached01:11211", "memcached02:11211"]

```elixir
results = Cream.Cluster.with_conn cluster, keys, fn conn, keys ->
Memcache.multi_get(conn, keys)
def MyClient do
use Cream.Client
end
# results will be a list of whatever was returned by the invocations of the given function.
```

Basically, Cream will group keys by memcached server and then call the provided
function for each group and return a list of the results of each call.

## Ruby compatibility

By default, Dalli uses Marshal to encode values stored in memcached, which
Elixir can't understand. So you have to change the serializer to something like JSON:
iex(1)> {:ok, _client} = MyClient.start_link()
{:ok, #PID<0.265.0>}

Ruby
```ruby
client = Dalli::Client.new(
["host01:11211", "host2:11211"],
serializer: JSON,
)
client.set("foo", 100)
```
iex(1)> MyClient.set({"foo", "bar"})
:ok

Elixir
```elixir
{:ok, cluster} = Cream.Cluster.start_link(
servers: ["host01:11211", "host2:11211"],
memcachex: [coder: Memcache.Coder.JSON]
)
Cream.Cluster.get(cluster, "foo")
# => "100"
```

So now both Ruby and Elixir will read/write to the memcached cluster in JSON,
but still beware! There are some differences between how Ruby and Elixir parse
JSON. For example, if you write an integer with Ruby, Ruby will read an integer,
but Elixir will read a string.

## Supervision

Everything is supervised, even the supervisors, so it really does make a
supervision tree.

A "cluster" is really a poolboy pool of cluster supervisors. A cluster
supervisor supervises each `Memcache.Connection` process and one
`Cream.Cluster.Worker` process.

No pids are stored anywhere, but instead processes are tracked via Elixir's
`Registry` module.

The results of `Cream.Cluster.start_link` and `MyClusterModule.start_link` can
be inserted into your application's supervision tree.

## Instrumentation

Cream uses [Instrumentation](https://hexdocs.pm/instrumentation) for... well,
instrumentation. It's default logging is hooked into this package. You can do
your own logging (or instrumentation) very easily.

```elixir
config :my_app, MyCluster,
log: false

Instrumentation.subscribe "cream", fn tag, payload ->
Logger.debug("cream.#{tag} took #{payload[:duration]} ms")
end
iex(1)> MyClient.get(client, "foo")
{:ok, "bar"}
```

## Running the tests
Expand All @@ -238,17 +50,19 @@ Test dependencies:
* Bundler

Then run...
```
bundle install
```sh
docker-compose up -d
bundle install --path vendor/bundle
bundle exec ruby test/support/populate.rb

mix test

# Stop and clean up containers
docker-compose stop
docker-compose rm
```

## TODO
## Todo

* Server weights
* Parallel memcached requests
30 changes: 1 addition & 29 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,31 +1,3 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :cream, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:cream, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
import Config

import_config "#{Mix.env}.exs"
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use Mix.Config
import Config

config :cream, servers: ["localhost:11211", "127.0.0.1:11211"]

Expand Down
23 changes: 23 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Config

{json, 0} = System.cmd("docker", ~w(compose ps --format json))

containers = Jason.decode!(json)

servers =[
"cream_ex-memcached-1",
"cream_ex-memcached-2",
"cream_ex-memcached-3"
]
|> Enum.map(fn name ->
container = Enum.find(containers, & &1["Name"] == name)

port = container["Publishers"]
|> List.first()
|> Map.get("PublishedPort")

"localhost:#{port}"
end)

config :cream, Cream.Connection, server: List.first(servers)
config :cream, Cream.Client, servers: servers
9 changes: 3 additions & 6 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use Mix.Config
import Config

config :cream, Test.Cluster,
servers: ["localhost:11201", "localhost:11202", "localhost:11203"],
memcachex: [coder: Memcache.Coder.JSON]
config :cream, TestClient, coder: Cream.Coder.Jason

config :logger,
level: :info
config :logger, :level, :info
Loading