|
-
CSharedMemCache
Introduction
CSharedMemCache is a name-based key-value memory cache that can be shared across processes.
It is best suited to a small-to-medium, read-heavy/write-light, same-session cache shared by a handful of trusted VB6 processes, where values are expensive to generate, invalidation is explicit, and contention is modest.
It is not a substitute for a full-featured cache such as Redis—with expiration, eviction, statistics, enumeration, clustering, and other operational features—or a persistent, transactional datastore such as SQLite.
Background
Skip this bit if you aren't interested in story time.
I'm working on a multi-process web application server layer for my existing VB6 backend code and it needs to generate HTML for presentation. Some of that HTML is expensive to generate but rarely (if ever) changes, so I figured some kind of cache was called for.
Many years ago, Olaf Schmidt introduced me to file mappings as a way to share data across processes, using a serializable RC6.cCollection as a key-value store, so I started there. The problem was that, when generating a value took a long time, multiple processes could attempt to generate the same value at the same time. This worked, but it was obviously inefficient.
More recently, I created a MutexEx project and figured it could be used to block additional generation attempts while one was already in progress, then release the waiting processes when the work was complete. That would allow only one process to perform the expensive operation while the others waited for the result.
After a bit of work, the approach did the job, and I felt somewhat like how a real programmer must feel when they use foundational building blocks to support a higher-level program. 
I could (maybe should!) have stopped there, but I like to share, so I thought I would clean things up a bit and post the project here for anyone else who might find it useful.
The first step was to run the code past ChatGPT for review. It did a very helpful job of identifying some potential pitfalls and helping me understand and correct them. Not that LLMs need any more advertising, but this step likely saved many hours of debugging some gnarly cross-process issues that would otherwise have been difficult to reproduce reliably, so I highly recommend it to other mere mortal programmers.
The next realization was that, with Olaf having (sadly!) largely disappeared from the VB6 community, most people are understandably avoiding taking on RC6 as a new dependency. This left the slightly daunting task of implementing a serializable Collection/Dictionary class of my own.
SIDE NOTE: existing RC6 users can swap RC6.cCollection back into this project and use it instead of the included implementation. RC6.cCollection is more feature-rich, more battle-tested, and still faster in a some areas.
Those earlier feelings of being a real programmer now seemed like a dream, so I turned back to ChatGPT. Armed with Olaf's cHashD code, I prompted ChatGPT to use that as inspiration while adding serialization/deserialization for the supported VB6 intrinsic types, arrays, and COM objects implementing the standard IPersist* interfaces. Ideally, it would support both binary and JSON serialization (as a bonus).
About 45 minutes and a swath of rain forest later, ChatGPT produced something that *almost* compiled!
After another 30 minutes or so of back-and-forth, the bloody thing worked. Crazy.
Over the course of the rest of the day, we worked through performance improvements (which started out quite poorly but quickly became much better), added features, cleaned up the structure and API, and tackled the genuinely difficult problems such as naming things. 
Performance still isn't quite at Olaf levels in every category, but the gap has been reduced from an order of magnitude or more in the initial pass to a much more reasonable percentage. In some operations, the new implementation actually appears to be faster (at least in limited synthetic testing).
I was now a manager! This means I can take all the credit, right? After roughly a day of work, the supporting serializable collection class was complete, along with a reasonably extensive set of tests to give me the "warm and fuzzies".
CSerializableCollection Features
The serializable collection class is the base storage system for the CSharedMemCache. It acts as a replacement for the RC6.cCollection class (partial only - just the methods required for CSharedMemCache - an RC6.cCollection can be substituted in for it if you prefer). CSerializableCollection includes:
- Binary serialization and deserialization
- JSON parsing and generation
- Compact Base64-wrapped JSON (ideal for database storage)
- Human-readable and editable JSON, with minified, spaces, and tabs formatting options
- Serialization of supported VB6 intrinsic values
- Serialization of Multidimensional arrays, including non-zero lower bounds
- Serialization Variant arrays containing nested supported values
- COM persistence through IPersistStream and IPersistStreamInit
- Readable COM persistence through IPersistPropertyBag and IPersistPropertyBag2
- A temporary in-memory property-bag bridge
- A native string dictionary used by the collection and persistence code
- Correctness, round-trip, contention, and performance tests. NOTE: Test modules are included in the ZIP, but not the project. You can add them to the project and execute TestAll if you want to ensure everything passes yourself.
Objects supporting property-bag persistence can be represented as named values in readable JSON. Objects supporting only stream persistence are preserved as opaque Base64 data, along with the class information needed to recreate them.
-
Re: CSharedMemCache
Now the meat:
CSharedMemCache
CSharedMemCache is a cross-process, in-memory key/value cache for VB6 applications.
Unlike a normal Collection or dictionary, the cache contents are stored in named Windows shared-memory mappings. Multiple processes running in the same Windows session can therefore open the same named cache and share calculated values, configuration, rendered content, lookup data, or other reusable results.
One common use case is a server application running several listener processes. When one process performs an expensive operation—such as generating an HTML fragment, loading metadata, or building a report—it can cache the result so the other processes can reuse it rather than independently performing the same work.
The cache is intended for trusted, cooperating processes. It uses a named session mutex to ensure that reads and writes do not overlap and corrupt the shared data. Antagonistic processes that know your cache name can easily overwrite cached values and cause untold chaos, so it is preferred for use in trusted environments.
Possible uses
- Caching expensive HTML fragments or complete rendered pages across several HTTP listener processes.
- Sharing configuration, metadata, lookup tables, endpoint names, templates, or formatting results.
- Avoiding repeated database queries or external-service calls when data is known not to have changed.
- Sharing calculation heavy report output.
- Cross-process memoization where the same expensive value might otherwise be generated concurrently.
Main features
Cross-process sharing
All processes using the same cache name access the same cached values.
The cache uses the Windows Local\ namespace, so sharing is limited to processes running in the same Windows session. Cross-session and service-to-user sharing are intentionally not supported at this time. Would any of you find cross-session/user-to-service useful? If so, we can discuss...
Dynamically resizable storage
By nature, Windows file mappings cannot be resized after creation - a limitation this class addresses. When the cache outgrows its current mapping, CSharedMemCache creates a larger versioned mapping, copies the complete serialized cache into it, and publishes the new version through a small shared control mapping. Processes can safely transition to the replacement mapping while older mappings remain valid until no process is using them, at which point they automatically disappear.
Optional shrinking
The cache grows automatically when required but does not automatically contract. The Shrink method can be used to replace an oversized mapping with a smaller one while retaining some spare capacity for future growth.
Compute-once caching
TakeForAdd acquires the cache lock and checks whether a key already exists.
When the value is missing, the caller can generate and add it while continuing to hold the lock. Other processes requesting the cache wait until that operation is complete and then reuse the newly cached value. This prevents several processes from performing the same expensive work following a cache miss.
Batched operations
Take returns a CSharedMemCacheHold object that keeps the cache locked. Once "held", several additions, removals, or other operations can then be performed against one in-memory copy of the cache. The collection is serialized only once when the holder is released, which can considerably improve performance for groups of related changes.
Automatic lock release
A CSharedMemCacheHold releases its lock when Release is called or when the holder object is destroyed.
Calling Release explicitly is recommended (but not required as release occurs automatically when the object goes out of scope) so any serialization or save error can then be reported to the caller rather than being suppressed during object termination.
Case-insensitive keys
Cache keys are case-insensitive:
Code:
"MyKey" = "mykey" = "MYKEY"
Keys are required, and positional access by numeric index is not currently supported.
Insert-or-replace behaviour
Add replaces the existing value when the supplied key is already present.
This differs from VB6.Collection.Add, which normally raises an error for a duplicate key.
Supported values
Values are serialized through CSerializableCollection.
Supported values include common Variant-compatible data types, arrays, and persistable COM objects implementing IPersistStream or IPersistStreamInit.
Save and restore
The complete serialized cache content can be retrieved or assigned through the Content property.
The Save and Load convenience methods can also write the cache to a file or restore it from one.
-
Re: CSharedMemCache
CSharedMemCache public interface
Init
Code:
Public Sub Init( _
ByVal p_CacheName As String, _
Optional ByVal p_CacheBytesStart As Long = 524288, _
Optional ByVal p_CacheBytesExpand As Long = 1048576)
Initializes the cache. This MUST be called before calling any other methods.
p_CacheName identifies the shared cache. Every process that should access the same cache must use the same name.
p_CacheBytesStart specifies the initial and minimum mapping size. p_CacheBytesExpan` specifies how much capacity is added whenever the cache must grow.
Init may only be called once for a particular CSharedMemCache instance.
Take
Code:
Public Function Take() As CSharedMemCacheHold
Acquires the cache lock and returns a holder that keeps it acquired.
Use this when performing several related operations. Changes are made against one loaded copy of the cache and serialized when the holder is released.
Example:
Code:
With SharedMemCache.Take
.SharedMemCache.Add "first-key", l_FirstValue
.SharedMemCache.Add "second-key", l_SecondValue
.SharedMemCache.Remove "old-key"
.Release
End With
TakeForAdd
Code:
Public Function TakeForAdd( _
ByVal p_Key As String) As CSharedMemCacheHold
Acquires the cache lock and checks whether a particular key exists.
The returned holder exposes KeyExists, KeyValue, and Add, allowing a value to be generated only when it is missing.
Example:
Code:
With SharedMemCache.TakeForAdd( _
"project-summary-" & l_ProjectGuid)
If Not .KeyExists Then
.Add GenerateExpensiveProjectHtml(l_ProjectGuid)
End If
l_Html = .KeyValue
.Release
End With
Add
Code:
Public Sub Add( _
ByVal p_Key As String, _
ByVal p_Value As Variant)
Adds a value to the cache.
When the key already exists, its previous value is replaced.
Remove
Code:
Public Sub Remove( _
ByVal p_Key As String)
Removes the value associated with the supplied key.
No error is raised when the key does not exist.
Value
Code:
Public Function Value( _
ByVal p_Key As String) As Variant
Returns the value associated with the supplied key.For RC6.cCollection/VB6 Collection users, this behaves similarly to the .Item method (but Key only - without Index access).
When the key does not exist, the method returns an empty string.
Value is read-only. Use Add to insert or replace a cached value.
Exists
Code:
Public Function Exists( _
ByVal p_Key As String) As Boolean
Returns True when the supplied key exists in the cache.
Key comparison is case-insensitive.
Clear
Removes every entry from the cache. For RC6.cCollection users, this behaves similarly to the .RemoveAll method.
Shrink
Code:
Public Sub Shrink()
Creates a smaller replacement mapping when the current mapping has substantially more capacity than the serialized cache requires.
The replacement retains some spare space based on the configured expansion size.
The cache does not shrink automatically, so this method is intended for occasional maintenance rather than normal use.
Content - Get
Code:
Public Property Get Content() As Variant
Returns the complete serialized cache as a Byte array contained in a Variant.
This can be used for backup, transport, persistence, or duplication of the cache content.
Content - Let
Code:
Public Property Let Content( _
pa_Content As Variant)
Replaces the complete cache using previously serialized Byte-array content.
The supplied content must use the serialization format produced by CSerializableCollection.
Save
Code:
Public Sub Save( _
ByVal p_Path As String)
Writes the complete serialized cache content to a file.
Load
Code:
Public Sub Load( _
ByVal p_Path As String)
Loads serialized cache content from a previously saved file, replacing the current cache contents.
CSharedMemCacheHold public interface
CSharedMemCacheHold represents an acquired cache lock.
It is returned by either Take or TakeForAdd and should not normally be instantiated directly.
SharedMemCache
Code:
Public Property Get SharedMemCache() _
As CSharedMemCache
Returns the owning CSharedMemCache instance.
This is primarily used with holders returned by Take to perform several cache operations while continuing to hold the same lock.
Key
Code:
Public Property Get Key() As String
Returns the key supplied to TakeForAdd.
This property is only valid for holders returned by TakeForAdd.
KeyExists
Code:
Public Property Get KeyExists() As Boolean
Returns True when the key supplied to TakeForAdd was found in the cache.
After calling the holder’s Add method, this property also returns True.
This property is only valid for holders returned by TakeForAdd.
KeyValue
Code:
Public Property Get KeyValue() As Variant
Returns the value associated with the key supplied to TakeForAdd.
The value may be one that already existed or one added through the holder’s Add method.
An error is raised when no value exists.
Add
Code:
Public Sub Add( _
p_Value As Variant)
Adds or replaces the value using the key supplied to TakeForAdd.
The holder’s KeyExists and KeyValue properties are updated so the newly generated value can be retrieved immediately.
This method is only valid for holders returned by TakeForAdd.
Release
Code:
Public Sub Release()
Ends the locked batch, saves any pending changes, releases the mutex, and detaches the holder from its owning cache.
Calling Release explicitly is recommended so any save error is returned to the caller, but it is not required (it will be called automatically when the object goes out of scope). Calling it more than once has no effect.
Optional shared-instance helper
MSharedMemCache provides a simple process-local singleton wrapper for applications that only require one shared cache instance.
SharedMemCache
Code:
Public Function SharedMemCache( _
Optional ByVal p_CacheName As String = vbNullString, _
Optional ByVal p_CacheBytesStart As Long = 524288, _
Optional ByVal p_CacheBytesExpand As Long = 1048576) _
As CSharedMemCache
Creates and initializes the process-local cache instance on its first call, then returns the same instance on later calls.
The cache name is required on the first call:
Code:
SharedMemCache "MyApplicationCache"
After initialization, it can be accessed without repeating the parameters:
Code:
SharedMemCache.Add "my-key", l_Value
ClearSharedMemCache`
Code:
Public Sub ClearSharedMemCache()
Clears every entry from the shared singleton cache.
-
Re: CSharedMemCache
PROJECT SOURCE CODE
SharedMemCache.zip
Enjoy!
-
Addicted Member
Re: CSharedMemCache
Hello! JPBRO. Thank you for sharing your excellent work. This 'CSharedMemCache' is very useful to me, and I have already added it to my own 'code repository'. Thanks again!
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|