contextual_config 0.1.0 → 0.2.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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +64 -7
- data/lib/contextual_config/concern/lookupable.rb +92 -26
- data/lib/contextual_config/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca628e95f0ec561248db3eb04ce08f9192784c6745b2a8a3b0679a8a2d47cfe1
|
|
4
|
+
data.tar.gz: 0b4c30ef65f92b82cb0746b68067d85324ce4154b1db035b286db52802af5307
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a62db7b905b29b4d5a04bd9bf1a090426ac81276ca4c555c14fa90d932e98cc97d8bbfe25789a026a98ea48d5efd5b98a4459946ffe2d8c62d7574f2a16a85ac
|
|
7
|
+
data.tar.gz: 349f24f5d3a87beb861b0d063349ab9fb1372d398d9aa5abe00c0f75f1d7dafbf6956659cd1de88fa86c314354199fcb44cf78d2a161a4f2d1b937d20c298f5e
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -665,21 +665,71 @@ Benefits::InsuranceConfig.create!(
|
|
|
665
665
|
|
|
666
666
|
## Performance Considerations
|
|
667
667
|
|
|
668
|
+
### Caching System
|
|
669
|
+
|
|
670
|
+
ContextualConfig includes a sophisticated two-level caching system for optimal performance:
|
|
671
|
+
|
|
672
|
+
#### 1. Candidates Cache
|
|
673
|
+
Caches the database query results (list of potential configurations) to avoid repeated database hits for the same key/context combinations.
|
|
674
|
+
|
|
675
|
+
#### 2. Results Cache
|
|
676
|
+
Caches the final matched configuration after processing all matching logic, providing the fastest possible lookups for repeated requests.
|
|
677
|
+
|
|
678
|
+
### Cache Configuration
|
|
679
|
+
|
|
680
|
+
```ruby
|
|
681
|
+
# Enable caching globally
|
|
682
|
+
ContextualConfig.configure do |config|
|
|
683
|
+
config.cache_enabled = true
|
|
684
|
+
config.cache_ttl = 300 # 5 minutes
|
|
685
|
+
config.cache_store = Rails.cache
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Fine-grained cache control in your models
|
|
689
|
+
class CustomConfig < ApplicationRecord
|
|
690
|
+
include ContextualConfig::Concern::Configurable
|
|
691
|
+
include ContextualConfig::Concern::Lookupable
|
|
692
|
+
|
|
693
|
+
protected
|
|
694
|
+
|
|
695
|
+
# Override to disable candidate caching for specific scenarios
|
|
696
|
+
def self.cache_candidates?(key:, context:)
|
|
697
|
+
# Only cache for stable contexts, not time-sensitive ones
|
|
698
|
+
!context.key?(:current_timestamp)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Override to disable result caching for specific scenarios
|
|
702
|
+
def self.cache_results?(key:, context:)
|
|
703
|
+
# Cache results except for admin users who need real-time data
|
|
704
|
+
context[:user_role] != 'admin'
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Override to customize candidate selection
|
|
708
|
+
def self.lookup_candidates(key:, context:)
|
|
709
|
+
# Custom logic for fetching candidates
|
|
710
|
+
active.where(key: key.to_s, department: context[:department]).order_by_priority
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
```
|
|
714
|
+
|
|
668
715
|
### Benchmarks
|
|
669
716
|
|
|
670
717
|
Based on testing with the JisrHR application:
|
|
671
718
|
|
|
672
|
-
- **Average lookup time**: ~3ms per configuration lookup
|
|
673
|
-
- **
|
|
719
|
+
- **Average lookup time**: ~3ms per configuration lookup (uncached)
|
|
720
|
+
- **Cached lookup time**: ~0.1ms per configuration lookup
|
|
721
|
+
- **100 concurrent lookups**: Completed in ~0.3 seconds (uncached), ~0.03 seconds (cached)
|
|
674
722
|
- **Database indexes**: Automatically created by the generator for optimal performance
|
|
675
|
-
- **Memory usage**: Minimal - configurations are loaded on-demand
|
|
723
|
+
- **Memory usage**: Minimal - configurations are loaded on-demand with intelligent caching
|
|
676
724
|
|
|
677
725
|
### Optimization Tips
|
|
678
726
|
|
|
679
|
-
1. **
|
|
680
|
-
2. **
|
|
681
|
-
3. **
|
|
682
|
-
4. **
|
|
727
|
+
1. **Enable caching**: Use the built-in two-level caching system for production deployments
|
|
728
|
+
2. **Use appropriate indexes**: The generator creates optimal indexes for common queries
|
|
729
|
+
3. **Customize cache behavior**: Override cache control methods for fine-grained performance tuning
|
|
730
|
+
4. **Limit scoping rule complexity**: Simpler rules = faster matching
|
|
731
|
+
5. **Use database-level constraints**: Leverage PostgreSQL JSONB indexes for complex queries
|
|
732
|
+
6. **Monitor cache hit rates**: Use logging to monitor cache effectiveness
|
|
683
733
|
|
|
684
734
|
### Production Deployment
|
|
685
735
|
|
|
@@ -740,6 +790,13 @@ Provides configuration lookup methods:
|
|
|
740
790
|
- `find_applicable_config(key:, context:)` - Find best match
|
|
741
791
|
- `find_all_applicable_configs(context:)` - Find all matches
|
|
742
792
|
|
|
793
|
+
**Protected Methods for Customization:**
|
|
794
|
+
|
|
795
|
+
- `lookup_candidates(key:, context:)` - Override to customize candidate selection for specific keys
|
|
796
|
+
- `lookup_all_candidates(context:)` - Override to customize candidate selection for all configurations
|
|
797
|
+
- `cache_candidates?(key:, context:)` - Override to control candidate caching behavior
|
|
798
|
+
- `cache_results?(key:, context:)` - Override to control result caching behavior
|
|
799
|
+
|
|
743
800
|
#### ContextualConfig::Concern::SchemaDrivenValidation
|
|
744
801
|
|
|
745
802
|
Provides JSON schema validation:
|
|
@@ -17,18 +17,17 @@ module ContextualConfig
|
|
|
17
17
|
# and fetches only those.
|
|
18
18
|
# @return [ActiveRecord::Base, nil] The instance of the best matching configuration, or nil if no match.
|
|
19
19
|
def find_applicable_config(key:, context:, fetch_all_of_key: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
20
|
-
# Check cache first if enabled
|
|
21
|
-
if
|
|
20
|
+
# Check result cache first if enabled
|
|
21
|
+
if cache_results?(key: key, context: context)
|
|
22
|
+
cache_key = generate_cache_key(key, context)
|
|
22
23
|
begin
|
|
23
|
-
cache_key = generate_cache_key(key, context)
|
|
24
24
|
cached_result = ContextualConfig.configuration.cache_store.read(cache_key)
|
|
25
25
|
if cached_result
|
|
26
|
-
log_lookup_attempt(key, context, '
|
|
26
|
+
log_lookup_attempt(key, context, 'result_cache_hit')
|
|
27
27
|
return cached_result
|
|
28
28
|
end
|
|
29
|
-
rescue => e
|
|
30
|
-
|
|
31
|
-
log_cache_error('read', e)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
log_lookup_attempt(key, context, "result_cache_error: #{e.message}")
|
|
32
31
|
end
|
|
33
32
|
end
|
|
34
33
|
|
|
@@ -41,9 +40,39 @@ module ContextualConfig
|
|
|
41
40
|
raise(NoMethodError, "#{self.name} must include ContextualConfig::Concern::Configurable to use Lookupable.")
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
# Check candidates cache if enabled
|
|
44
|
+
candidates = nil
|
|
45
|
+
if cache_candidates?(key: key, context: context)
|
|
46
|
+
candidates_cache_key = generate_candidates_cache_key(key, context)
|
|
47
|
+
begin
|
|
48
|
+
candidates = ContextualConfig.configuration.cache_store.read(candidates_cache_key)
|
|
49
|
+
if candidates
|
|
50
|
+
log_lookup_attempt(key, context, 'candidates_cache_hit')
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
log_lookup_attempt(key, context, "candidates_cache_error: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
45
56
|
|
|
46
|
-
candidates
|
|
57
|
+
# Fetch candidates from database if not cached
|
|
58
|
+
if candidates.nil?
|
|
59
|
+
log_lookup_attempt(key, context, 'database_lookup')
|
|
60
|
+
candidates = lookup_candidates(key: key, context: context)
|
|
61
|
+
# Cache candidates if enabled
|
|
62
|
+
if cache_candidates?(key: key, context: context)
|
|
63
|
+
candidates_cache_key = generate_candidates_cache_key(key, context)
|
|
64
|
+
begin
|
|
65
|
+
ContextualConfig.configuration.cache_store.write(
|
|
66
|
+
candidates_cache_key,
|
|
67
|
+
candidates.to_a, # Convert to array to avoid caching AR relations
|
|
68
|
+
expires_in: ContextualConfig.configuration.cache_ttl
|
|
69
|
+
)
|
|
70
|
+
log_lookup_attempt(key, context, 'candidates_cache_stored')
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log_lookup_attempt(key, context, "candidates_cache_write_error: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
47
76
|
|
|
48
77
|
# Delegate to the ContextualMatcher service to find the best match from the candidates.
|
|
49
78
|
# The service will handle the logic of iterating through candidates, evaluating scoping_rules,
|
|
@@ -54,18 +83,17 @@ module ContextualConfig
|
|
|
54
83
|
)
|
|
55
84
|
|
|
56
85
|
# Cache the result if caching is enabled
|
|
57
|
-
if
|
|
86
|
+
if cache_results?(key: key, context: context) && result
|
|
87
|
+
cache_key = generate_cache_key(key, context)
|
|
58
88
|
begin
|
|
59
|
-
cache_key = generate_cache_key(key, context)
|
|
60
89
|
ContextualConfig.configuration.cache_store.write(
|
|
61
90
|
cache_key,
|
|
62
91
|
result,
|
|
63
92
|
expires_in: ContextualConfig.configuration.cache_ttl
|
|
64
93
|
)
|
|
65
|
-
log_lookup_attempt(key, context, '
|
|
66
|
-
rescue => e
|
|
67
|
-
|
|
68
|
-
log_cache_error('write', e)
|
|
94
|
+
log_lookup_attempt(key, context, 'result_cache_stored')
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
log_lookup_attempt(key, context, "result_cache_write_error: #{e.message}")
|
|
69
97
|
end
|
|
70
98
|
end
|
|
71
99
|
|
|
@@ -87,7 +115,7 @@ module ContextualConfig
|
|
|
87
115
|
log_lookup_attempt('all_configs', context, 'all_configs_lookup')
|
|
88
116
|
|
|
89
117
|
# Fetch all active configurations of this type, ordered by priority.
|
|
90
|
-
all_candidates =
|
|
118
|
+
all_candidates = lookup_all_candidates(context: context)
|
|
91
119
|
|
|
92
120
|
# Delegate to the ContextualMatcher service.
|
|
93
121
|
ContextualConfig::Services::ContextualMatcher.find_all_matches(
|
|
@@ -96,6 +124,48 @@ module ContextualConfig
|
|
|
96
124
|
)
|
|
97
125
|
end
|
|
98
126
|
|
|
127
|
+
protected
|
|
128
|
+
|
|
129
|
+
# Override this method in your configuration class to customize candidate selection
|
|
130
|
+
# for specific key lookups. This allows fine-grained control over which configurations
|
|
131
|
+
# are considered for matching.
|
|
132
|
+
#
|
|
133
|
+
# @param key [String, Symbol] The configuration key to look for
|
|
134
|
+
# @param context [Hash] The lookup context
|
|
135
|
+
# @return [ActiveRecord::Relation] The candidates for matching
|
|
136
|
+
def lookup_candidates(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
137
|
+
active.where(key: key.to_s).order_by_priority
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Override this method in your configuration class to customize candidate selection
|
|
141
|
+
# for all configurations (used by find_all_applicable_configs).
|
|
142
|
+
#
|
|
143
|
+
# @param context [Hash] The lookup context
|
|
144
|
+
# @return [ActiveRecord::Relation] All candidates for matching
|
|
145
|
+
def lookup_all_candidates(context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
146
|
+
active.order_by_priority
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Override this method to control whether candidates should be cached.
|
|
150
|
+
# By default, candidates are cached when caching is enabled globally.
|
|
151
|
+
#
|
|
152
|
+
# @param key [String, Symbol] The configuration key
|
|
153
|
+
# @param context [Hash] The lookup context
|
|
154
|
+
# @return [Boolean] Whether to cache candidates for this lookup
|
|
155
|
+
def cache_candidates?(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
156
|
+
ContextualConfig.configuration.cache_enabled?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Override this method to control whether results should be cached.
|
|
160
|
+
# By default, results are cached when caching is enabled globally.
|
|
161
|
+
#
|
|
162
|
+
# @param key [String, Symbol] The configuration key
|
|
163
|
+
# @param context [Hash] The lookup context
|
|
164
|
+
# @return [Boolean] Whether to cache the final result for this lookup
|
|
165
|
+
def cache_results?(key:, context:) # rubocop:disable Lint/UnusedMethodArgument
|
|
166
|
+
ContextualConfig.configuration.cache_enabled?
|
|
167
|
+
end
|
|
168
|
+
|
|
99
169
|
private
|
|
100
170
|
|
|
101
171
|
# Generate cache key for configuration lookup
|
|
@@ -104,6 +174,12 @@ module ContextualConfig
|
|
|
104
174
|
"contextual_config:#{self.name}:#{key}:#{Digest::MD5.hexdigest(context_hash.to_s)}"
|
|
105
175
|
end
|
|
106
176
|
|
|
177
|
+
# Generate cache key for candidates
|
|
178
|
+
def generate_candidates_cache_key(key, context)
|
|
179
|
+
context_hash = context.is_a?(Hash) ? context.sort.to_h : context
|
|
180
|
+
"contextual_config_candidates:#{self.name}:#{key}:#{Digest::MD5.hexdigest(context_hash.to_s)}"
|
|
181
|
+
end
|
|
182
|
+
|
|
107
183
|
# Log configuration lookup attempts
|
|
108
184
|
def log_lookup_attempt(key, context, operation)
|
|
109
185
|
return unless ContextualConfig.configuration.enable_logging?
|
|
@@ -113,16 +189,6 @@ module ContextualConfig
|
|
|
113
189
|
"ContextualConfig::Lookupable: #{operation} for #{self.name} - Key: #{key}, Context: #{context}"
|
|
114
190
|
)
|
|
115
191
|
end
|
|
116
|
-
|
|
117
|
-
# Log cache errors
|
|
118
|
-
def log_cache_error(operation, error)
|
|
119
|
-
return unless ContextualConfig.configuration.enable_logging?
|
|
120
|
-
|
|
121
|
-
logger = ContextualConfig.configuration.effective_logger
|
|
122
|
-
logger.warn(
|
|
123
|
-
"ContextualConfig::Lookupable: Cache #{operation} error - #{error.class}: #{error.message}"
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
192
|
end
|
|
127
193
|
end
|
|
128
194
|
end
|