Results 1 to 10 of 10

Thread: CSharedMemCache

  1. #1

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    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.

  2. #2

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    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.

  3. #3

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    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

    Code:
    Public Sub 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.

  4. #4

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    Re: CSharedMemCache

    PROJECT SOURCE CODE

    SharedMemCache2.zip

    Enjoy!
    Last edited by jpbro; Today at 10:31 AM.

  5. #5
    Addicted Member
    Join Date
    Oct 2014
    Posts
    136

    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!

  6. #6

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    Re: CSharedMemCache

    Glad you found it useful @smileyoufu

    PS: Updated the ZIP to remove an unused RC6 reference that was left over from testing. There are no functionality changes in the latest update, so no need to re-download if you either removed the reference yourself or have RC6.dll registered (in which case the unneeded reference won't cause any problems, but feel free to remove it).

  7. #7
    Fanatic Member
    Join Date
    Jun 2016
    Location
    España
    Posts
    639

    Re: CSharedMemCache

    Thank you very much.
    I'm having some issues with the application, I'll try again later.
    Regards

    PD:Try Claude is the best

  8. #8

    Thread Starter
    PowerPoster
    Join Date
    Aug 2010
    Location
    Canada
    Posts
    2,906

    Re: CSharedMemCache

    @yokesee - What issues?

  9. #9
    Fanatic Member
    Join Date
    Jun 2016
    Location
    España
    Posts
    639

    Re: CSharedMemCache

    1. Create a key "test1" with a value.
    2. Get the result it works fine.
    3. Remove the key "test1".
    4. Get "test1" (infinite loop).

  10. #10
    PowerPoster VanGoghGaming's Avatar
    Join Date
    Jan 2020
    Location
    Eve Online - Mining, Missions & Market Trading!
    Posts
    2,651

    Talking Re: CSharedMemCache

    There's also a built-in method to accomplish most of this: RoCreatePropertySetSerializer, this API function takes care of serializing/deserializing an IPropertySet which is an observable collection with string keys:

    Code:
    public interface IPropertySet : IDictionary<string,object>, IEnumerable<KeyValuePair<string,object>>, IObservableMap<string,object>
    So most of the heavy lifting is already taken care of, leaving you only with the shared memory mapping details to contend with.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width