-
Notifications
You must be signed in to change notification settings - Fork 33
Add caching to remote JWKS fetch #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
9a6aa7e
feat: add in-memory cache module for storing jwk set
nicknisi edd71a3
add tests for cache implementation
nicknisi ce0fa68
add test to confirm jwks is cached
nicknisi eef1dec
add doc comments to cache.rb
nicknisi c2f5db1
add docstring comment to Entry constructor
nicknisi b7b2637
be more explicit with expires_in variable name
nicknisi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module WorkOS | ||
| # The Cache module provides a simple in-memory cache for storing values | ||
| # This module is not meant to be instantiated in a user space, and is used internally by the SDK | ||
| module Cache | ||
| # The Entry class represents a cache entry with a value and an expiration time | ||
| class Entry | ||
| attr_reader :value, :expires_at | ||
|
|
||
| # Initializes a new cache entry | ||
| # @param value [Object] The value to store in the cache | ||
| # @param expires_in_seconds [Integer, nil] The expiration time for the value in seconds, or nil for no expiration | ||
| def initialize(value, expires_in_seconds) | ||
| @value = value | ||
| @expires_at = expires_in_seconds ? Time.now + expires_in_seconds : nil | ||
| end | ||
|
|
||
| # Checks if the entry has expired | ||
| # @return [Boolean] True if the entry has expired, false otherwise | ||
| def expired? | ||
| return false if expires_at.nil? | ||
|
|
||
| Time.now > @expires_at | ||
| end | ||
| end | ||
|
|
||
| class << self | ||
| # Fetches a value from the cache, or calls the block to fetch the value if it is not present | ||
| # @param key [String] The key to fetch the value for | ||
| # @param expires_in [Integer] The expiration time for the value in seconds | ||
| # @param force [Boolean] If true, the value will be fetched from the block even if it is present in the cache | ||
| # @param block [Proc] The block to call to fetch the value if it is not present in the cache | ||
| # @return [Object] The value fetched from the cache or the block | ||
| def fetch(key, expires_in: nil, force: false, &block) | ||
| entry = store[key] | ||
|
|
||
| if force || entry.nil? || entry.expired? | ||
| value = block.call | ||
| store[key] = Entry.new(value, expires_in) | ||
| return value | ||
| end | ||
|
|
||
| entry.value | ||
| end | ||
|
|
||
| # Reads a value from the cache | ||
| # @param key [String] The key to read the value for | ||
| # @return [Object] The value read from the cache, or nil if the value is not present or has expired | ||
| def read(key) | ||
| entry = store[key] | ||
| return nil if entry.nil? || entry.expired? | ||
|
|
||
| entry.value | ||
| end | ||
|
|
||
| # Writes a value to the cache | ||
| # @param key [String] The key to write the value for | ||
| # @param value [Object] The value to write to the cache | ||
| # @param expires_in [Integer] The expiration time for the value in seconds | ||
| # @return [Object] The value written to the cache | ||
| def write(key, value, expires_in: nil) | ||
| store[key] = Entry.new(value, expires_in) | ||
| value | ||
| end | ||
|
|
||
| # Deletes a value from the cache | ||
| # @param key [String] The key to delete the value for | ||
| def delete(key) | ||
| store.delete(key) | ||
| end | ||
|
|
||
| # Clears all values from the cache | ||
| def clear | ||
| store.clear | ||
| end | ||
|
|
||
| # Checks if a value exists in the cache | ||
| # @param key [String] The key to check for | ||
| # @return [Boolean] True if the value exists and has not expired, false otherwise | ||
| def exist?(key) | ||
| entry = store[key] | ||
| !(entry.nil? || entry.expired?) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # The in-memory store for the cache | ||
| def store | ||
| @store ||= {} | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| describe WorkOS::Cache do | ||
| before { described_class.clear } | ||
|
|
||
| describe '.write and .read' do | ||
| it 'stores and retrieves data' do | ||
| described_class.write('key', 'value') | ||
| expect(described_class.read('key')).to eq('value') | ||
| end | ||
|
|
||
| it 'returns nil if key does not exist' do | ||
| expect(described_class.read('missing')).to be_nil | ||
| end | ||
| end | ||
|
|
||
| describe '.fetch' do | ||
| it 'returns cached value when present and not expired' do | ||
| described_class.write('key', 'value') | ||
| fetch_value = described_class.fetch('key') { 'new_value' } | ||
| expect(fetch_value).to eq('value') | ||
| end | ||
|
|
||
| it 'executes block and caches value when not present' do | ||
| fetch_value = described_class.fetch('key') { 'new_value' } | ||
| expect(fetch_value).to eq('new_value') | ||
| end | ||
|
|
||
| it 'executes block and caches value when force is true' do | ||
| described_class.write('key', 'value') | ||
| fetch_value = described_class.fetch('key', force: true) { 'new_value' } | ||
| expect(fetch_value).to eq('new_value') | ||
| end | ||
| end | ||
|
|
||
| describe 'expiration' do | ||
| it 'expires values after specified time' do | ||
| described_class.write('key', 'value', expires_in: 0.1) | ||
| expect(described_class.read('key')).to eq('value') | ||
| sleep 0.2 | ||
| expect(described_class.read('key')).to be_nil | ||
| end | ||
|
|
||
| it 'executes block and caches new value when expired' do | ||
| described_class.write('key', 'old_value', expires_in: 0.1) | ||
| sleep 0.2 | ||
| fetch_value = described_class.fetch('key') { 'new_value' } | ||
| expect(fetch_value).to eq('new_value') | ||
| end | ||
|
|
||
| it 'does not expire values when expires_in is nil' do | ||
| described_class.write('key', 'value', expires_in: nil) | ||
| sleep 0.2 | ||
| expect(described_class.read('key')).to eq('value') | ||
| end | ||
| end | ||
|
|
||
| describe '.exist?' do | ||
| it 'returns true if key exists' do | ||
| described_class.write('key', 'value') | ||
| expect(described_class.exist?('key')).to be true | ||
| end | ||
|
|
||
| it 'returns false if expired' do | ||
| described_class.write('key', 'value', expires_in: 0.1) | ||
| sleep 0.2 | ||
| expect(described_class.exist?('key')).to be false | ||
| end | ||
|
|
||
| it 'returns false if key does not exist' do | ||
| expect(described_class.exist?('missing')).to be false | ||
| end | ||
| end | ||
|
|
||
| describe '.delete' do | ||
| it 'deletes key' do | ||
| described_class.write('key', 'value') | ||
| described_class.delete('key') | ||
| expect(described_class.read('key')).to be_nil | ||
| end | ||
| end | ||
|
|
||
| describe '.clear' do | ||
| it 'removes all keys from the cache' do | ||
| described_class.write('key1', 'value1') | ||
| described_class.write('key2', 'value2') | ||
|
|
||
| described_class.clear | ||
|
|
||
| expect(described_class.read('key1')).to be_nil | ||
| expect(described_class.read('key2')).to be_nil | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.