diff --git a/README.md b/README.md index 715d2ee..cdfe079 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Shmop is an easy to use set of functions that allows PHP to read, write, create Shared memory an IPC1 mechanism native to UNIX. In essence, it’s about two processes sharing a common segment of memory that they can both read to and write from to communicate with one another. +Locks and semaphores are used to ensure atomic access so that multiple PHP processes can concurrently use the same shared memory safely. + ## Installing Require this package, with [Composer](https://getcomposer.org/), in the root directory of your project. @@ -62,4 +64,28 @@ Cache::store('memory')->put('some_key', ['value' => 'text']); use Illuminate\Support\Facades\Cache; $data = Cache::store('memory')->get('some_key'); -``` \ No newline at end of file +``` + +## About memory limits +Garbage collection (by removing expired items) will be performed when the cache is near the size limit. +If the garbage collection fails to reduce the size of the cache below the size limit, +then the cache will be invalidated and the underlying memory segment is marked for deletion. + +Running out of memory will generate a warning or a notice in your logs, no matter if it is resolved by +a garbage collection or by segment deletion. + +Note: **items that are stored as "forever" may be removed when the cache reaches its size limit**. + +### Recreating the memory block +When recreating the memory block, the newest size limit defined in the Laravel config file will be used. + +### Manually marking the memory segment for deletion +There are use cases to this, such as wanting to refresh the memory block now instead of waiting for +another "out of memory" event. In this case, you may do the following: + +```php +// the deletion will be managed by the OS kernel , and will happen at a future time +Cache::store('memory')->getStore()->requestDeletion(); +``` + +This usage will not trigger any warnings or notices since this is an action taken deliberately. diff --git a/composer.json b/composer.json index 908f543..2c14262 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "require": { "php" : "^7.3|^8.0", "ext-shmop": "*", + "ext-sysvsem": "*", "illuminate/support": "^8.0|^9.0", "illuminate/cache": "^8.0|^9.0" }, diff --git a/src/Cache/MemoryBlock.php b/src/Cache/MemoryBlock.php index 17eba62..03f18a2 100644 --- a/src/Cache/MemoryBlock.php +++ b/src/Cache/MemoryBlock.php @@ -189,6 +189,11 @@ public function getMode() /** * Gets the current shared memory size. * + * Note that this is the size specified by the user. + * + * This size can be different from the size obtained from getSizeInMemory() + * because, e.g., the user has changed the size limit but the change is not yet communicated to the OS kernel. + * * @return int */ public function getSize() @@ -196,6 +201,22 @@ public function getSize() return $this->size; } + /** + * Gets the current shared memory size. + * + * Note that this is the size of the actual memory block managed by the OS kernel. + * + * This size can be different from the size obtained from getSize(). + * When the specified memory size is updated by the user, + * the OS must delete and recreate the memory block so that the new size can be applied. + * + * @return int + */ + public function getSizeInMemory() + { + return shmop_size($this->id); + } + /** * Makes a System V IPC key from pathname and a project identifier. * diff --git a/src/Cache/MemoryStore.php b/src/Cache/MemoryStore.php index 86eb4f9..46dc6c0 100644 --- a/src/Cache/MemoryStore.php +++ b/src/Cache/MemoryStore.php @@ -16,12 +16,22 @@ class MemoryStore implements StoreInterface /** @var \Sanchescom\Cache\MemoryBlock */ protected $memory; + /** @var resource */ + private $semaphore; + + /** @var bool */ + private $ignoreNextLock; + /** * @param \Sanchescom\Cache\MemoryBlock */ public function __construct(MemoryBlock $memoryBlock) { $this->memory = $memoryBlock; + + $this->semaphore = sem_get(ftok(__FILE__, 's')); + + $this->ignoreNextLock = false; } /** @@ -59,6 +69,8 @@ public function get($key) /** * Store an item in the cache for a given number of seconds. * + * This action is atomic. + * * @param string $key * @param mixed $value * @param int $seconds @@ -67,6 +79,13 @@ public function get($key) */ public function put($key, $value, $seconds) { + sem_acquire($this->semaphore, $this->ignoreNextLock); + + if ($this->ignoreNextLock) { + // needed to handle "increment from nothing" case + $this->ignoreNextLock = false; + } + $storage = $this->getStorage(); if ($storage === false) { @@ -81,12 +100,16 @@ public function put($key, $value, $seconds) $this->setStorage($storage); + sem_release($this->semaphore); + return true; } /** * Increment the value of an item in the cache. * + * This action is atomic. + * * @param string $key * @param mixed $value * @@ -94,9 +117,13 @@ public function put($key, $value, $seconds) */ public function increment($key, $value = 1) { + sem_acquire($this->semaphore); + $storage = $this->getStorage(); if (!$storage || !isset($storage[$key])) { + $this->ignoreNextLock = true; + $this->forever($key, $value); return $storage[$key]['value']; @@ -106,12 +133,16 @@ public function increment($key, $value = 1) $this->setStorage($storage); + sem_release($this->semaphore); + return $storage[$key]['value']; } /** * Decrement the value of an item in the cache. * + * This action is atomic (managed by increment()). + * * @param string $key * @param mixed $value * @@ -125,6 +156,8 @@ public function decrement($key, $value = 1) /** * Store an item in the cache indefinitely. * + * This action is atomic (managed by put()). + * * @param string $key * @param mixed $value * @@ -138,12 +171,16 @@ public function forever($key, $value) /** * Remove an item from the cache. * + * This action is atomic. + * * @param string $key * * @return bool */ public function forget($key) { + sem_acquire($this->semaphore); + $storage = $this->getStorage(); if ($storage === false) { @@ -156,26 +193,44 @@ public function forget($key) $this->setStorage($storage); + sem_release($this->semaphore); + return true; } + sem_release($this->semaphore); + return false; } /** * Remove all items from the cache. * + * This action is atomic. + * * @return bool */ public function flush() { + sem_acquire($this->semaphore); + $this->setStorage([]); + sem_release($this->semaphore); + return true; } /** - * Save data in memory storage + * Save data in memory storage. + * + * If the given data will exceed the in-system memory block size limit, + * then a garbage collection (GC) is performed on the data array where expired items are discarded. + * A new data array containing the survivors of the GC will be created. + * + * After the GC, if the new data array will still exceed the in-system memory block size limit, + * then the in-system memory block will be marked for deletion. + * Updated settings, e.g. the updated memory size, will then be applied when the memory block is recreated. * * @param $data * @@ -183,11 +238,40 @@ public function flush() */ protected function setStorage($data) { - $this->memory->write($this->serialize($data)); + $serial = (string) $this->serialize($data); + $memorySize = $this->memory->getSizeInMemory(); + + if (strlen($serial) > $memorySize) { + $timeNow = $this->currentTime(); + + foreach ($data as $key => $details) { + $expiresAt = $details['expiresAt'] ?? 0; + + if ($expiresAt !== 0 && $timeNow > $expiresAt) { + unset($data[$key]); + } + } + + $message = "Laravel Memory Cache: Out of memory (Unix allocated $memorySize); "; + + $serial = (string) $this->serialize($data); + + if (strlen($serial) > $memorySize) { + $message .= 'the segment will be recreated.'; + trigger_error($message, E_USER_WARNING); + + $this->memory->delete(); + } else { + $message .= 'garbage collection was performed.'; + trigger_error($message, E_USER_NOTICE); + } + } + + $this->memory->write($serial); } /** - * Get data from memory storage + * Get data from memory storage. * * @return array */ @@ -253,4 +337,19 @@ protected function unserialize($value) { return is_numeric($value) ? $value : @unserialize($value); } + + /** + * Requests to the OS kernel that the underlying shared memory segment should be deleted. + * + * This allows the memory segment to be recreated later with updated parameters. + * + * The timing of deletion is managed by the OS kernel, but this usually happens after + * all relevant processes are disconnected from the shared memory segment. + * + * @return void + */ + public function requestDeletion() + { + $this->memory->delete(); + } }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: