NAME

Module::Generic::Global - Contextual global storage by namespace, class or object

SYNOPSIS

use Module::Generic::Global ':const';

# Class-level global repository
my $repo = Module::Generic::Global->new( 'errors' => 'My::Module' );
$repo->set( $exception );
my $err = $repo->get;

# Object-level global repository
my $repo2 = Module::Generic::Global->new( 'cache' => $obj );
$repo2->set( { foo => 42 } );
my $data = $repo2->get;

# System-level repository
# Here 'system' is a special keyword
my $repo = Module::Generic::Global->new( 'system_setting' => 'system' );

# Inside Some::Module:
$repo->set( $some_value );

# Inside Another::Module
my $repo = Module::Generic::Global->new( 'system_setting' => 'system' );
my $value = $repo->get; # $some_value retrieved

{
    $repo->lock;
    # do something safely
} # lock released at end of scope

# With limits
my $limited = Module::Generic::Global->new( 'bounded' => 'My::Module',
    max_size        => 10,          # max keys in namespace
    max_store_bytes => 1024,        # max bytes per stored item (serialised)
    max_total_bytes => 4096         # max bytes total in namespace (serialised)
);
$limited->set( 'data' ); # Evicts if over

# With auto-cleanup (mod_perl)
my $auto = Module::Generic::Global->new( 'temp' => 'My::Module',
    auto_cleanup => 1  # Auto-registers cleanup for self namespace
);
# Or with options
my $custom_auto = Module::Generic::Global->new(
    'multi' => 'My::Module',
    auto_cleanup =>
    {
        namespaces => ['temp', 'cache'],
        keep       => ['persistent'],
        callback   => sub
        {
            my( $repo, $r ) = @_;
            $r->log->notice( "Cleanup for " . $repo->namespace );
        },
    }
);

# Manual cleanup register (mod_perl)
$repo->cleanup_register( r => $apache_r ); # clears default namespace unless overridden
$repo->cleanup_register(
    r          => $apache_r,
    namespaces => ['extra'],
    keep       => ['save'],
    callback   => sub { ... },
);

VERSION

v1.1.0

DESCRIPTION

This module provides contextual, thread/process-safe global storage, organised by namespace and a context key derived from a class name, object identity, or the special system context. Supports Perl ithreads or APR-based threading environments.

It has no dependencies except for Scalar::Util and Storable::Improved

It is designed to work in:

  • single-process non-threaded Perl

  • Perl ithreads (threads / threads::shared)

  • mod_perl under threaded MPMs (Worker/Event), including a fallback to APR::ThreadRWLock when shared variables are not available

Values are serialised before storage (default Storable::Improved) and deserialised on retrieval.

The repository used is locked in read or write mode before being accessed ensuring no collision and integrity.

It is designed to store one value at a time in the specified namespace in the global repository.

Notes on context and scope

The repository is stored in memory. This means:

  • Under a multi-process server (such as Apache), the system context is per-process.

  • Under threads, values may be shared within a process depending on the runtime environment and the locking backend.

In other words: system means "system within this process/runtime context", not "system across all server processes".

CONSTRUCTOR

# System-level repository
# 'system' is a special keyword
my $repo = Module::Generic::Global->new( 'global_settings' => 'system' );

# Class-level global repository
my $repo = Module::Generic::Global->new( 'errors' => 'My::Module' );
my $repo = Module::Generic::Global->new( 'errors' => 'My::Module', key => $unique_key );
my $repo = Module::Generic::Global->new( 'errors' => 'My::Module', { key => $unique_key } );

# Object-level global repository
my $repo2 = Module::Generic::Global->new( 'cache' => $obj );
my $repo2 = Module::Generic::Global->new( 'cache' => $obj, key => $unique_key );
my $repo2 = Module::Generic::Global->new( 'cache' => $obj, { key => $unique_key } );

# Declaring a callback used by the get() method
my $repo = Module::Generic::Global->new( 'cache' => $obj,
    key => $unique_key,
    on_get => sub
    {
        my $cache = shift( @_ );
        my $repo  = $_; # The object is accessible as $_
    },
    serialiser => 'Sereal',
);

# Or by providing some values in the callback
my $repo = Module::Generic::Global->new( 'cache' => $obj,
    key => $unique_key,
    on_get => [sub
        {
            my $cache = shift( @_ );
            my $repo  = $_; # The object is accessible as $_
        },
        host => 'localhost', user => 'john', password => 'some secret', port => 12345
    ]
);

# or by providing the arguments as an array reference
my $repo = Module::Generic::Global->new( 'cache' => $obj,
    key => $unique_key,
    on_get => [sub
        {
            my $cache = shift( @_ );
            my $repo  = $_; # The object is accessible as $_
        },
        [ host => 'localhost', user => 'john', password => 'some secret', port => 12345 ]
    ]
);

new

Creates a new repository under a given namespace, and context, and return the new class instance.

A context key is composed of:

1. the class name, or the object ID retrieved with "refaddr" in Scalar::Util if a blessed object was provided,
2. the current process ID, and
3. optionally the thread tid if running under a thread.

However, if a context is system, then the key is also automatically set to system.

For object-level repositories, when running under Perl ithreads and threads is loaded, the thread id is included in the default key.

Possible options

  • auto_cleanup

    Enable auto cleanup mode. This is disabled by default. When enabled, this will empty the global repository. When running under mod_perl, this will call cleanup_register using "request" in Apache2::RequestUtil. If GlobalRequest of mod_perl is not enabled, or an error occurs, it will be caught, so this is safe to use.

    Supported values are:

    • 0 (default)

    • 1 (enable with default behaviour)

    • a hash reference of options passed to "cleanup_register"

    Example:

    Module::Generic::Global->new( 'cache' => $module_class, 
        auto_cleanup =>
        {
            namespaces => ['ns1','ns2'],
            keep => ['persistent'],
            callback => sub
            {
                my( $repo, $r ) = @_;
                $r->log->notice( "Module::Generic::Global clean up ", $repo->namespace, " occurred." );
            }
        }
    );

    auto_cleanup does not accept a plain CODE reference as a mode; use the hashref form and pass callback instead.

  • key

    Specifies explicitly a key to use instead of the default computed key.

    Please note that this option would be discarded if the context is set to system

  • max_size

    Maximum number of keys in a namespace (integer > 0). If exceeded on set, older entries are evicted.

    Eviction order is based on the internal ordering list, which is updated on successful set. Overwriting an existing key refreshes its recency.

  • max_store_bytes

    Maximum allowed size (in bytes) for a single stored value (integer > 0). The size is based on the serialised representation (and for scalars, the raw scalar size is also checked).

    Default: 5MB (5242880).

  • max_total_bytes

    Maximum allowed total size (in bytes) for all stored values in a namespace (integer > 0). If exceeded on set, older entries are evicted until the new total fits, otherwise set fails.

    Eviction order is based on the internal ordering list, which is updated on successful set. Overwriting an existing key refreshes its recency.

  • on_get

    Registers a callback that is automatically executed after a successful call to "get".

    This is useful to restore contextual dependencies after deserialisation (e.g. re-attaching a database connection object).

    The callback can be provided as:

    • a code reference

      on_get => sub { ... }
    • an array reference of callback + arguments

      on_get => [
          sub { my( $value, @args ) = @_; ... },
          [ 'arg1', 'arg2' ],
      ];

      or:

      on_get => [
          sub { my( $value, @args ) = @_; ... },
          'arg1',
          'arg2',
      ];

    When the callback is executed, $_ is locally set to the current repository object instance.

    This allows the callback to access the repository without altering the callback argument list.

    Example:

    on_get => sub
    {
        my( $value ) = @_;
        my $repo = $_;
        return( $value );
    };

    You may override the callback per-call by passing on_get to "get".

  • refcount

    When the refcount option is provided (boolean 0/1/empty string or undef), the repository enables reference counting for automatic cleanup.

    Module::Generic::Global will count increment on creation and decrement on destroy—entries remove when hitting 0. Implicitly enabled for class-level contexts (string controller, no key) to manage shared lifetime. For keyed or object-level, opt-in via refcount => 1.

  • serialiser or serializer

    Specify a serialiser to use instead of the default value set in the global variable $DEFAULT_SERIALISER, which is, by default, set to Storable::Improved

    The serialiser will be used to freeze and thaw the data.

    Supported serialiser are:

    • CBOR or CBOR::XS

    • CBOR::Free

    • Sereal

    • Storable or Storable::Improved

    • A CODE reference, called as $serialiser->( $data, 'freeze' ) or $serialiser->( $data, 'thaw' )

    When a code reference is provided, the following arguments will be provided:

    1. the data to serialise or to deserialiser
    2. the key word freeze or thaw depending on the action.

METHODS

cleanup_register

# In your Apache/mod_perl2 script
sub handler : method
{
    my( $class, $r ) = @_;
    my $repo = Module::Generic::Global->new( 'errors' => 'My::Module' );
    $repo->cleanup_register( $r );
    # Rest of your code
}

This prepares a cleanup callback to empty the global variables when the Apache/mod_perl2 request is complete.

It takes an Apache2::RequestRec as its sole argument.

clear_error

$repo->clear_error;
Module::Generic::Global->clear_error;

This clear the error for the current object, and the latest recorded error stored as a global variable.

error

$repo->error( "Something went wrong: ", $some_value );
my $exception = $repo->error;

Used as a mutator, and this sets an exception object, and returns undef in scalar context, or an empty list in list context.

In accessor mode, this returns the currently set exception object, if any.

exists

Returns true (1) if a value is currently stored under the context, o false (0) otherwise. This only checks that an entry exists, not whether that entry has a true value.

get

Retrieves the stored value, deserialising it using the preferred serialiser if it was serialised, and return it.

If an error occurs, it returns undef in scalar context, or an empty list in list context.

The callback registered via the on_get option (constructor) is executed after deserialisation. You may also override the callback on a per-call basis by passing on_get to get().

Examples

Basic usage

my $repo = Module::Generic::Global->new( table_cache => 'system' );
my $cache = $repo->get;

Using an on_get callback declared once at construction time

This is useful when your cached objects require contextual restoration after deserialisation.

my $repo = Module::Generic::Global->new(
    table_cache => 'system',
    on_get => sub
    {
        my( $value ) = @_;
        my $repo = $_;

        # Example of a post-thaw adjustment
        # $value->some_init_method if( ref( $value ) );

        return( $value );
    }
);

my $cache = $repo->get;

Passing extra arguments to the callback

my $repo = Module::Generic::Global->new(
    table_cache => 'system',
    on_get => [
        sub
        {
            my( $value, $prefix ) = @_;
            my $repo = $_;
            # Do something with $value and $prefix
            return( $value );
        },
        "tables-cache: ",
    ],
);

my $cache = $repo->get;

Overriding the callback explicitly at get() time

This overrides the callback that was optionally provided at construction.

my $cache = $repo->get(
    on_get => sub
    {
        my( $value ) = @_;
        my $repo = $_;
        return( $value );
    }
);

Re-attaching a database connection after deserialisation

This pattern is typical when serialising objects that should not store runtime resources such as DBI handles.

my $repo = Module::Generic::Global->new(
    table_cache => 'system',
    on_get => sub
    {
        my( $tables ) = @_;
        my $repo = $_;

        return if( !ref( $tables ) );

        # Example: rebind a connection object after thaw
        # $tables->dbo( $dbo );

        return( $tables );
    }
);

my $tables = $repo->get;

key

Returns the computed or explicit key used by this instance.

This is read-only.

length

my $repo = Module::Generic::Global->new( 'my_repo' => 'My::Module' );
say $repo->length;

Returns the number of keys currently present in the namespace (as seen in the repository).

lock

{
    $repo->lock;
    # Do some computing
    # Lock is freed automatically when it gets out of scope
}

Sets a lock to ensure the manipulation done is thread-safe. If the code runs in a single thread environment, then this does not do anything.

When the lock gets out of scope, it is automatically removed.

namespace

Returns the current namespace used in this instance. This is read-only.

remove

Removes the stored value for the current namespace and key, updates ordering and size accounting, and removes per-key stats.

This can also be called as clear

set

$repo->set( { foo => 42 } );

Stores a scalar or serialisable reference in the current namespace and context. This overwrite any previous value for the same context.

The value provided is serialised using the preferred serialiser before it is stored in the global repository.

On success, updates:

  • per-namespace total bytes accounting

  • per-key stored byte stats

  • ordering list (the key is moved to the end on every successful set, including overwrites)

If limits are configured (max_store_bytes, max_total_bytes, max_size), older entries may be evicted on set.

Returns true upon success, and upon error, return undef in scalar context, or an empty list in list context.

stat

Returns an array reference of hash references describing the largest entries in $STATS (sorted by stored bytes), limited by the provided limit (default 20).

It contains the following properties:

  • bytes

    The size in bytes for the current value.

  • key

    The repository key.

unlock

$repo->unlock;

This is used to remove the lock set when under Apache2 ModPerl by using "unlock" in APR::ThreadRWLock

It is usually not necessary to call this explicitly, because when the lock set previously gets out of scope, it is automatically removed.

CONSTANTS

The constants can be imported into your namespace with:

use Module::Generic::Global ':const';

CAN_THREADS

This returns true (1) or false (0) depending on whether Perl was compiled with ithreads (Interpreter Threads) or not.

HAS_THREADS

This returns true (1) or false (0) depending on whether Perl was compiled with ithreads (Interpreter Threads) or not, and whether threads has been loaded.

This is not actually a constant. Its value will change if threads has been loaded or not. For example:

use Module::Generic::Global ':const';

say HAS_THREADS ? 'yes' : 'no'; # no
require threads;
say HAS_THREADS ? 'yes' : 'no'; # yes

IN_THREAD

This returns true (1) or false (0) depending on whether Perl was compiled with ithreads (Interpreter Threads) or not, and whether threads has been loaded, and we are inside a thread (tid returns a non-zero value). For example:

use Module::Generic::Global ':const';

say IN_THREAD ? 'yes' : 'no'; # no
require threads;
say IN_THREAD ? 'yes' : 'no'; # no
my $thr = threads->create(sub
{
    say IN_THREAD ? 'yes' : 'no'; # yes
});
$thr->join;

Note that this only works for Perl threads

MOD_PERL

This returns the ModPerl version if running under ModPerl, or undef otherwise.

MPM

This returns the Apache MPM (Multi-Processing Modules) used if running under ModPerl. Possible values are prefork, worker, event, winnt or undef if not running under ModPerl.

This uses "show" in Apache2::MPM to make that determination.

HAS_MPM_THREADS

This returns true (1) or false (0) depending on whether the code is running under ModPerl, and the Apache MPM (Multi-Processing Modules) used is threaded (e.g. worker, or event). This uses "is_threaded" in Apache2::MPM to make that determination.

See Apache2::MPM

THREAD & PROCESS SAFETY

This module is designed to be fully thread-safe and process-safe, ensuring data integrity across Perl ithreads and mod_perl’s threaded Multi-Processing Modules (MPMs) such as Worker or Event. It uses robust synchronisation mechanisms to prevent data corruption and race conditions in concurrent environments.

Synchronisation Mechanisms

Module::Generic::Global employs the following synchronisation strategies:

  • Perl ithreads

    When Perl is compiled with ithreads support (CAN_THREADS is true) and the threads module is loaded (HAS_THREADS is true), global repositories ($REPO, $ERRORS, $LOCKS) are marked :shared using threads::shared. Access to these repositories is protected by "lock" in perlfunc to ensure thread-safe read and write operations.

  • mod_perl Threaded MPMs

    In mod_perl environments with threaded MPMs (e.g., Worker or Event, where HAS_MPM_THREADS is true), the module uses APR::ThreadRWLock for locking if Perl lacks ithreads support or threads is not loaded, which is very unlikely, since mod_perl would normally would not work under threaded MPM if perl was not compiled with threads. This ensures thread-safety within Apache threads sharing the same process.

  • Non-Threaded Environments

    In single-threaded environments (e.g., mod_perl Prefork MPM or non-threaded Perl), locking is skipped, as no concurrent access occurs within a process. Data is isolated per-process via the process ID ($$) in context keys.

Shared Data Initialisation

To prevent race conditions during dynamic conversion of global variables to shared ones, the module adopts a conservative approach. At startup, if CAN_THREADS is true (Perl supports ithreads), the global repositories are initialised as :shared:

  • $REPO: Stores data for all namespaces and context keys.

  • $ERRORS: Stores error objects for error handling.

  • $LOCKS: Manages lock state for thread-safe operations.

This upfront initialisation ensures thread-safety without the risk of mid-air clashes that could occur if private globals were converted dynamically when threads are loaded.

Context Key Isolation

Data is stored in repositories using context keys that ensure isolation:

  • Class-Level Keys

    For class-level repositories (e.g., $class->new( 'ns' => 'My::Module' )), keys are formatted as <class>;<pid> (e.g., My::Module;1234). This isolates data per class and process, preventing cross-process interference.

  • Object-Level Keys

    For object-level repositories (e.g., $class->new( 'ns' => $obj )), keys are:

    - Non-Threaded: <refaddr>;<pid> (e.g., 1234567;1234), where refaddr is the object’s reference address from "refaddr" in Scalar::Util.
    - Threaded: <refaddr>;<pid>;<tid> (e.g., 1234567;1234;1), where tid is the thread ID from "tid" in threads.

    The inclusion of tid when HAS_THREADS is true ensures thread-level isolation for object-level data. Repositories created in non-threaded environments cannot be overwritten by threaded ones, and vice versa, due to differing key formats.

Error Handling

Errors are stored in both instance-level ($self->{_error}) and class-level ($ERRORS repository under the errors namespace) storage, supporting patterns like My::Module->new || die( My::Module->error ). Each class-process-thread combination (keyed by <class>;<pid>[;<tid>]) stores at most one error, with subsequent errors overwriting the previous entry to prevent memory growth. Errors are serialised using Storable::Improved for compatibility with threads::shared.

mod_perl Considerations

In mod_perl environments:

  • Prefork MPM

    Data is per-process, requiring no additional synchronisation, as each process operates independently.

  • Threaded MPMs (Worker/Event)

    Threads within a process share the same Perl interpreter clone, necessitating thread-safety. Since mod_perl requires threaded Perl ($Config{useithreads} true), threads::shared and "lock" in perlfunc are used unless threads is not loaded, in which case APR::ThreadRWLock is employed. Users should call "cleanup_register" in handlers to clear shared repositories after each request, preventing memory leaks.

  • Thread-Unsafe Functions

    Certain Perl functions (e.g., localtime, readdir, srand) and operations (e.g., chdir, umask, chroot) are unsafe in threaded MPMs, as they may affect all threads in a process. Users must avoid these and consult perlthrtut and mod_perl documentation for guidance.

Thread-Safety Considerations

The module’s thread-safety relies on:

  • Shared Repositories: Initialised as :shared when CAN_THREADS is true, ensuring safe access across threads.

  • Locking: "lock" in perlfunc or APR::ThreadRWLock protects all read/write operations.

  • Key Isolation: Thread-specific keys (<refaddr>;<pid>;<tid>) isolate object-level data when created in different threads.

  • Reference Counting

    When refcount is enabled upon instantiation, repositories use internal counts to auto-remove entries on last reference destroy, preventing leaks in shared contexts. Counts are per-namespace/key, thread-safe via locking.

In environments where %INC manipulation (e.g., by forks) emulates threads, HAS_THREADS and IN_THREAD may return true. This is generally safe, as forks provides a compatible tid method, but users in untrusted environments should verify $INC{'threads.pm'} points to the actual threads module.

For maximum safety, users running mod_perl with threaded MPMs should ensure Perl is compiled with ithreads and explicitly load threads, or use Prefork MPM for single-threaded operation.

Environment Variables

The following environment variables influence thread-safety:

  • MG_MAX_RETRIES: Sets the number of lock retry attempts (default: 10).

  • MG_RETRY_DELAY: Sets the base retry delay for data operations (microseconds, default: 10,000).

  • MG_ERROR_DELAY: Sets the base retry delay for error operations (microseconds, default: 5,000).

SEE ALSO

Module::Generic, Storable::Improved, Module::Generic::Exception

AUTHOR

Jacques Deguest <jack@deguest.jp>

COPYRIGHT & LICENSE

Copyright (c) 2025 DEGUEST Pte. Ltd.

You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.