gemstack-cache 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25a46e0050bdcee85e6a3885884b831e248dcd744522a1d8feb47c5f174c7904
4
- data.tar.gz: f4c8bc84c9e79ee9929b013224f5680f70f8a974e62dc1a4bf42e0a7ef88be99
3
+ metadata.gz: 4326f904f232d74d34126acef45bb27e5629ee8d1f7027e837f909b9a2bc9cfb
4
+ data.tar.gz: ddc9bcc0139cf7d102ec78a4f280888f834a014fa67174104d73b5f683e06d87
5
5
  SHA512:
6
- metadata.gz: 428fae85e4443fa3fb759a6aa7476ab5b4e0dbabcf32540584cde634709f1cd1726c28c946b9c20ae9a8f4fd8ed11e19fb3d2657168b8a31efcf23796e3a6c28
7
- data.tar.gz: 7fd29db35be70ee1979aa2d7e1293e83f1ee8610a3b34e7cd60a45ebf9e4be72dfcb3b33edecca7897930f3afc1f75276ac2d18e1a0fc6ab2336b71b18404436
6
+ metadata.gz: fc4d58e8b4e7815610f3f9ae0ead1a4505bd4e34a0aa0137bc4727cb132c7dccb856d25fabebd4c19201d6d7cbf3dae72987790ecb0fc15a6670dc35cca977e7
7
+ data.tar.gz: 94419821fe5956f92ece4907ab2f783beca7e1767fe79b9abd3d5975897431bc7b9d575a7b28dc4aed4de3ca3236fe1f846ffbabae86a3d7d068115d2fc96665
data/CHANGELOG.md CHANGED
@@ -1,29 +1,5 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.5
3
+ ## 0.3.0
4
4
 
5
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
6
-
7
- ## 0.2.4
8
-
9
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
10
-
11
- ## 0.2.3
12
-
13
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
14
-
15
- ## 0.2.2
16
-
17
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
18
-
19
- ## 0.2.1
20
-
21
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
22
-
23
- ## 0.2.0
24
-
25
- See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
26
-
27
- ## 0.1.0
28
-
29
- First release. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
5
+ Merged into the gemstack gem; this version is a transition shim. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
data/README.md CHANGED
@@ -1,23 +1,13 @@
1
1
  # gemstack-cache
2
2
 
3
- GemStack cache: GemStack.cache.fetch with memory, null and Redis stores.
3
+ **Merged into [`gemstack`](https://rubygems.org/gems/gemstack) in GemStack 0.3.0.**
4
4
 
5
- Part of [GemStack](https://github.com/gemstack-rb/gemstack), a modular Ruby API framework for Next.js
6
- applications by [Adware Technologies](https://www.adwaretech.com). All GemStack gems are developed together in that repository and released with the same
7
- version.
5
+ This version is a transition shim: it depends on `gemstack` and loads `gemstack/cache`, so
6
+ Gemfiles that still list `gemstack-cache` keep working. To finish upgrading, remove `gem "gemstack-cache"`
7
+ from your Gemfile (it's loaded by `require "gemstack"`).
8
8
 
9
- ## Installation
10
-
11
- Installed with the `gemstack` gem; you rarely need to add it yourself.
12
-
13
- ## Documentation
14
-
15
- - [Guide](https://github.com/gemstack-rb/gemstack/blob/main/docs/caching.md)
16
- - [All guides](https://github.com/gemstack-rb/gemstack/tree/main/docs) ·
17
- [Architecture](https://github.com/gemstack-rb/gemstack/blob/main/ARCHITECTURE.md)
18
-
19
- Source, issues and pull requests: [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack)
20
- (this gem lives in `gems/gemstack-cache`).
9
+ GemStack is a modular Ruby API framework for Next.js applications by
10
+ [Adware Technologies](https://www.adwaretech.com) — [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack).
21
11
 
22
12
  ## License
23
13
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # gemstack-cache is part of the gemstack gem since 0.3.0. This shim keeps Gemfiles
4
+ # that still list gemstack-cache working; remove the line from your Gemfile.
5
+ require "gemstack/cache"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemstack-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adware Technologies
@@ -11,19 +11,28 @@ cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: gemstack-core
14
+ name: gemstack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.5
19
+ version: 0.3.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '1.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - '='
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.3.0
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: 0.2.5
32
+ version: '1.0'
33
+ description: Since GemStack 0.3.0, gemstack-cache is part of the gemstack gem. This
34
+ version only depends on gemstack and loads gemstack/cache, so Gemfiles that still
35
+ list it keep working.
27
36
  email:
28
37
  - gemstack26@gmail.com
29
38
  executables: []
@@ -33,20 +42,18 @@ files:
33
42
  - CHANGELOG.md
34
43
  - LICENSE.txt
35
44
  - README.md
36
- - lib/gemstack/cache.rb
37
- - lib/gemstack/cache/memory_store.rb
38
- - lib/gemstack/cache/null_store.rb
39
- - lib/gemstack/cache/redis_store.rb
40
- - lib/gemstack/cache/store.rb
45
+ - lib/gemstack-cache.rb
41
46
  homepage: https://github.com/gemstack-rb/gemstack
42
47
  licenses:
43
48
  - MIT
44
49
  metadata:
45
50
  rubygems_mfa_required: 'true'
46
- source_code_uri: https://github.com/gemstack-rb/gemstack/tree/main/gems/gemstack-cache
47
- changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/gems/gemstack-cache/CHANGELOG.md
51
+ source_code_uri: https://github.com/gemstack-rb/gemstack
52
+ changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md
48
53
  bug_tracker_uri: https://github.com/gemstack-rb/gemstack/issues
49
54
  documentation_uri: https://github.com/gemstack-rb/gemstack/tree/main/docs
55
+ post_install_message: gemstack-cache is now part of the gemstack gem. Remove `gem
56
+ "gemstack-cache"` from your Gemfile (it's loaded by `require "gemstack"`).
50
57
  rdoc_options: []
51
58
  require_paths:
52
59
  - lib
@@ -63,5 +70,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
70
  requirements: []
64
71
  rubygems_version: 4.0.20
65
72
  specification_version: 4
66
- summary: 'GemStack cache: GemStack.cache.fetch with memory, null and Redis stores'
73
+ summary: Merged into the gemstack gem — remove gemstack-cache from your Gemfile
67
74
  test_files: []
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- module Cache
5
- # A thread-safe, per-process LRU cache with expiry. Values are stored
6
- # marshalled, so callers never share (and can't mutate) cached objects —
7
- # the same semantics as the Redis store. With several Puma workers each
8
- # has its own copy; use Redis to share a cache across processes.
9
- class MemoryStore < Store
10
- Entry = Struct.new(:data, :expires_at)
11
-
12
- def initialize(max_entries: 10_000, **)
13
- super(**)
14
- @max_entries = max_entries
15
- @entries = {}
16
- @mutex = Mutex.new
17
- end
18
-
19
- def size = @entries.size
20
- def clear = @mutex.synchronize { @entries.clear }
21
-
22
- def increment(key, by = 1, expires_in: default_expires_in)
23
- normalized = normalize(key)
24
- @mutex.synchronize do
25
- entry = live_entry(normalized)
26
- value = (entry ? Integer(load(entry.data)) : 0) + by
27
- store(normalized, value, entry ? entry.expires_at : expires_at(expires_in))
28
- value
29
- end
30
- end
31
-
32
- private
33
-
34
- def read_entry(key)
35
- @mutex.synchronize do
36
- entry = live_entry(key) or return MISSING
37
- @entries[key] = @entries.delete(key) # mark as most recently used
38
- load(entry.data)
39
- end
40
- end
41
-
42
- def write_entry(key, value, expires_in)
43
- data = dump(value)
44
- @mutex.synchronize { store_data(key, data, expires_at(expires_in)) }
45
- end
46
-
47
- def delete_entry(key) = @mutex.synchronize { !@entries.delete(key).nil? }
48
-
49
- # Callers hold the mutex.
50
- def live_entry(key)
51
- entry = @entries[key]
52
- return nil unless entry
53
- return entry unless entry.expires_at && entry.expires_at <= monotonic
54
-
55
- @entries.delete(key)
56
- nil
57
- end
58
-
59
- def store(key, value, expires_at) = store_data(key, dump(value), expires_at)
60
-
61
- def store_data(key, data, expires_at)
62
- @entries.delete(key)
63
- @entries[key] = Entry.new(data, expires_at)
64
- @entries.shift while @entries.size > @max_entries # Hash order = LRU order
65
- true
66
- end
67
- end
68
- end
69
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- module Cache
5
- # Caches nothing: fetch always runs its block. The default in tests, so
6
- # tests never depend on state cached by earlier tests.
7
- class NullStore < Store
8
- def clear = true
9
- def increment(_key, by = 1, **) = by
10
-
11
- private
12
-
13
- def read_entry(_key) = MISSING
14
- def write_entry(_key, _value, _expires_in) = true
15
- def delete_entry(_key) = false
16
- end
17
- end
18
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- module Cache
5
- # A cache shared by every process, through Redis. Uses the `redis-client`
6
- # gem (add `gem "redis-client"` to the Gemfile) with a connection pool
7
- # sized to the server's threads, or any client object you pass that
8
- # responds to #call(*command) (a redis-client pool or connection).
9
- #
10
- # config.cache.store = :redis # REDIS_URL
11
- # config.cache.store = GemStack::Cache::RedisStore.new(client: my_pool, namespace: "shop")
12
- class RedisStore < Store
13
- attr_reader :client
14
-
15
- def initialize(url: nil, client: nil, pool_size: 5, **)
16
- super(**)
17
- @client = client || build_client(url, pool_size)
18
- end
19
-
20
- def increment(key, by = 1, expires_in: default_expires_in)
21
- normalized = normalize(key)
22
- value = client.call("INCRBY", normalized, by)
23
- client.call("PEXPIRE", normalized, (Float(expires_in) * 1000).round, "NX") if expires_in
24
- value
25
- end
26
-
27
- # Deletes only keys in this store's namespace (never FLUSHDB).
28
- def clear
29
- raise Error, "RedisStore#clear needs a namespace (config.cache.namespace)" unless namespace
30
-
31
- cursor = "0"
32
- loop do
33
- cursor, keys = client.call("SCAN", cursor, "MATCH", "#{namespace}:*", "COUNT", 500)
34
- client.call("UNLINK", *keys) unless keys.empty?
35
- break if cursor == "0"
36
- end
37
- true
38
- end
39
-
40
- private
41
-
42
- def read_entry(key)
43
- data = client.call("GET", key)
44
- return MISSING if data.nil?
45
-
46
- # Counters are stored as plain integers so INCRBY works on them.
47
- data.match?(/\A-?\d+\z/) ? Integer(data) : load(data.b)
48
- end
49
-
50
- def write_entry(key, value, expires_in)
51
- data = value.is_a?(Integer) ? value.to_s : dump(value)
52
- if expires_in
53
- client.call("SET", key, data, "PX", (Float(expires_in) * 1000).round)
54
- else
55
- client.call("SET", key, data)
56
- end
57
- true
58
- end
59
-
60
- def delete_entry(key) = client.call("DEL", key).positive?
61
-
62
- def build_client(url, pool_size)
63
- require "redis-client"
64
- RedisClient.config(url: url).new_pool(size: pool_size, timeout: 1.0)
65
- rescue LoadError
66
- raise ConfigurationError, 'the Redis cache store needs `gem "redis-client"` in the Gemfile'
67
- end
68
- end
69
- end
70
- end
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
-
5
- module GemStack
6
- module Cache
7
- # The store interface. Subclasses implement read_entry / write_entry /
8
- # delete_entry / clear (and may override increment for atomicity).
9
- #
10
- # Keys may be strings, symbols, arrays of parts, or objects with
11
- # #cache_key (GemStack models have one); they are namespaced and hashed
12
- # when longer than MAX_KEY bytes. Values must be Marshal-able.
13
- class Store
14
- MAX_KEY = 200
15
- MISSING = Object.new.freeze
16
-
17
- attr_reader :namespace, :default_expires_in
18
-
19
- def initialize(namespace: nil, default_expires_in: nil)
20
- @namespace = namespace&.to_s
21
- @default_expires_in = default_expires_in
22
- end
23
-
24
- # Returns the cached value, or computes it with the block, stores and
25
- # returns it. nil results are cached too. force: true recomputes.
26
- def fetch(key, expires_in: default_expires_in, force: false)
27
- normalized = normalize(key)
28
- unless force
29
- value = read_entry(normalized)
30
- return value unless value.equal?(MISSING)
31
- end
32
- raise ArgumentError, "fetch needs a block on a cache miss" unless block_given?
33
-
34
- value = yield
35
- write_entry(normalized, value, expires_in)
36
- value
37
- end
38
-
39
- def read(key)
40
- value = read_entry(normalize(key))
41
- value.equal?(MISSING) ? nil : value
42
- end
43
-
44
- def write(key, value, expires_in: default_expires_in)
45
- write_entry(normalize(key), value, expires_in)
46
- value
47
- end
48
-
49
- def exist?(key) = !read_entry(normalize(key)).equal?(MISSING)
50
- def delete(key) = delete_entry(normalize(key))
51
-
52
- # Not atomic in the base class; stores override it where they can.
53
- def increment(key, by = 1, expires_in: default_expires_in)
54
- normalized = normalize(key)
55
- current = read_entry(normalized)
56
- value = (current.equal?(MISSING) ? 0 : Integer(current)) + by
57
- write_entry(normalized, value, expires_in)
58
- value
59
- end
60
-
61
- def decrement(key, by = 1, **) = increment(key, -by, **)
62
-
63
- def normalize(key)
64
- raw = key_part(key)
65
- full = namespace ? "#{namespace}:#{raw}" : raw
66
- full.bytesize > MAX_KEY ? "#{full[0, 100]}:sha256:#{Digest::SHA256.hexdigest(full)}" : full
67
- end
68
-
69
- private
70
-
71
- def key_part(key)
72
- case key
73
- when Array then key.map { |part| key_part(part) }.join("/")
74
- when String, Symbol, Numeric then key.to_s
75
- else key.respond_to?(:cache_key) ? key.cache_key.to_s : key.to_s
76
- end
77
- end
78
-
79
- def dump(value) = Marshal.dump(value)
80
- def load(data) = Marshal.load(data) # rubocop:disable Security/MarshalLoad -- only data this process wrote
81
-
82
- def expires_at(expires_in)
83
- expires_in ? monotonic + Float(expires_in) : nil
84
- end
85
-
86
- def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
- end
88
- end
89
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "gemstack/core"
4
-
5
- module GemStack
6
- # Application caching:
7
- #
8
- # GemStack.cache.fetch("product:#{id}", expires_in: 300) { Product.find(id) }
9
- # GemStack.cache.fetch([:stats, Date.today]) { expensive_report } # array keys
10
- # GemStack.cache.increment("signups")
11
- # GemStack.cache.delete("product:#{id}")
12
- #
13
- # Stores are swappable (config.cache.store): :memory (default), :null
14
- # (default in test), :redis, or any object implementing the Store interface.
15
- # Redis is never required.
16
- module Cache
17
- class Config < Settings
18
- setting :store, default: -> { GemStack.env.test? ? :null : :memory }
19
- # Prefix for every key ("shop:product:1"). Required for Redis#clear.
20
- setting :namespace, default: -> { GemStack.config.name }
21
- # Seconds; nil = no expiry unless given per call.
22
- setting :default_expires_in, default: nil
23
- # MemoryStore: maximum entries per process before the least recently used are evicted.
24
- setting :max_entries, default: 10_000
25
- setting :redis_url, default: -> { ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
26
- setting :redis_pool_size, default: -> { Integer(ENV.fetch("GEMSTACK_MAX_THREADS", 5)) }
27
- end
28
-
29
- class << self
30
- def build(setting = GemStack.config.cache.store, config: GemStack.config.cache)
31
- options = { namespace: config.namespace, default_expires_in: config.default_expires_in }
32
- case setting
33
- when :memory, "memory" then MemoryStore.new(max_entries: config.max_entries, **options)
34
- when :null, "null" then NullStore.new(**options)
35
- when :redis, "redis"
36
- RedisStore.new(url: config.redis_url, pool_size: config.redis_pool_size, **options)
37
- when Store then setting
38
- else
39
- raise ConfigurationError, "unknown cache store #{setting.inspect}" unless setting.respond_to?(:fetch)
40
-
41
- setting
42
- end
43
- end
44
- end
45
- end
46
-
47
- class << self
48
- def cache
49
- @cache ||= Cache.build
50
- end
51
-
52
- attr_writer :cache
53
- end
54
- end
55
-
56
- require_relative "cache/store"
57
- require_relative "cache/memory_store"
58
- require_relative "cache/null_store"
59
- require_relative "cache/redis_store"
60
-
61
- GemStack::Config.namespace(:cache, GemStack::Cache::Config)