From ab8af87ad8b5e8769c2b011aa6b43e51b9cc3b8b Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 21 Nov 2023 22:42:46 +0000 Subject: [PATCH 1/5] #1184: initial work for caching support, plus scoped variable --- README.md | 1 + docs/index.md | 2 +- examples/caching.ps1 | 31 ++++ src/Pode.psd1 | 17 ++- src/Private/Caching.ps1 | 76 ++++++++++ src/Private/Context.ps1 | 8 ++ src/Private/Helpers.ps1 | 39 +++++- src/Private/Server.ps1 | 4 + src/Public/Caching.ps1 | 303 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 examples/caching.ps1 create mode 100644 src/Private/Caching.ps1 create mode 100644 src/Public/Caching.ps1 diff --git a/README.md b/README.md index 9ba22b6c6..f22f752a6 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * Generate/bind self-signed certificates * Secret management support to load secrets from vaults * Support for File Watchers +* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 📦 Install diff --git a/docs/index.md b/docs/index.md index c5d175bd9..14f0c4acf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,7 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Support for dynamically building Routes from Functions and Modules * Generate/bind self-signed certificates * Secret management support to load secrets from vaults -* Support for File Watchers +* Support for File Watchers* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 🏢 Companies using Pode diff --git a/examples/caching.ps1 b/examples/caching.ps1 new file mode 100644 index 000000000..423188418 --- /dev/null +++ b/examples/caching.ps1 @@ -0,0 +1,31 @@ +$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) +Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8085 +Start-PodeServer -Threads 3 { + + # listen on localhost:8085 + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + # log errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Set-PodeCacheDefaultTtl -Value 60 + + # get cpu, and cache it + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + if ($null -ne $cache:cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # Write-PodeHost 'here - cached' + return + } + + $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # Write-PodeHost 'here - raw' + } + +} \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index cd0ba8d01..1627f580c 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -400,7 +400,22 @@ 'Use-PodeSemaphore', 'Enter-PodeSemaphore', 'Exit-PodeSemaphore', - 'Clear-PodeSemaphores' + 'Clear-PodeSemaphores', + + # caching + 'Get-PodeCache', + 'Set-PodeCache', + 'Test-PodeCache', + 'Remove-PodeCache', + 'Clear-PodeCache', + 'Add-PodeCacheStorage', + 'Remove-PodeCacheStorage', + 'Get-PodeCacheStorage', + 'Test-PodeCacheStorage', + 'Set-PodeCacheDefaultStorage', + 'Get-PodeCacheDefaultStorage', + 'Set-PodeCacheDefaultTtl', + 'Get-PodeCacheDefaultTtl' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 new file mode 100644 index 000000000..d5e702b3a --- /dev/null +++ b/src/Private/Caching.ps1 @@ -0,0 +1,76 @@ +function Get-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Metadata + ) + + $meta = $PodeContext.Server.Cache.Items[$Name] + if ($null -eq $meta) { + return $null + } + + # check ttl/expiry + if ($meta.Expiry -lt [datetime]::UtcNow) { + Remove-PodeCache -Name $Name + return $null + } + + # return value an metadata if required + if ($Metadata) { + return $meta + } + + # return just the value as default + return $meta.Value +} + +function Set-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [object] + $InputObject, + + [Parameter()] + [int] + $Ttl = 0 + ) + + # crete (or update) value value + $PodeContext.Server.Cache.Items[$Name] = @{ + Value = $InputObject + Ttl = $Ttl + Expiry = [datetime]::UtcNow.AddSeconds($Ttl) + } +} + +function Test-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.Items.ContainsKey($Name) +} + +function Remove-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $null = $PodeContext.Server.Cache.Items.Remove($Name) +} + +function Clear-PodeCacheInternal { + $null = $PodeContext.Server.Cache.Items.Clear() +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index cacdb4cbe..d5ac517fd 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -254,6 +254,14 @@ function New-PodeContext { # shared state between runspaces $ctx.Server.State = @{} + # setup caching + $ctx.Server.Cache = @{ + Items = @{} + Storage = @{} + DefaultStorage = $null + DefaultTtl = 3600 # 1hr + } + # output details, like variables, to be set once the server stops $ctx.Server.Output = @{ Variables = @{} diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index b9805ace1..a2fa9c23c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2435,7 +2435,7 @@ function Convert-PodeScopedVariables { $PSSession, [Parameter()] - [ValidateSet('State', 'Session', 'Secret', 'Using')] + [ValidateSet('State', 'Session', 'Secret', 'Using', 'Cache')] $Skip ) @@ -2467,6 +2467,10 @@ function Convert-PodeScopedVariables { $ScriptBlock = Invoke-PodeSecretScriptConversion -ScriptBlock $ScriptBlock } + if (($null -eq $Skip) -or ($Skip -inotcontains 'Cache')) { + $ScriptBlock = Invoke-PodeCacheScriptConversion -ScriptBlock $ScriptBlock + } + # return if (($null -ne $Skip) -and ($Skip -icontains 'Using')) { return $ScriptBlock @@ -2542,6 +2546,39 @@ function Invoke-PodeSecretScriptConversion { return $ScriptBlock } +function Invoke-PodeCacheScriptConversion { + param( + [Parameter()] + [scriptblock] + $ScriptBlock + ) + + # do nothing if no script + if ($null -eq $ScriptBlock) { + return $ScriptBlock + } + + # rename any $secret: vars + $scriptStr = "$($ScriptBlock)" + $found = $false + + while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+)\s*=)') { + $found = $true + $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Name '$($Matches['name'])' -InputObject ") + } + + while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+))') { + $found = $true + $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Name '$($Matches['name'])')") + } + + if ($found) { + $ScriptBlock = [scriptblock]::Create($scriptStr) + } + + return $ScriptBlock +} + function Invoke-PodeSessionScriptConversion { param( [Parameter()] diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 295a62c25..4c7b0fcfb 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -242,6 +242,10 @@ function Restart-PodeInternalServer { # clear up shared state $PodeContext.Server.State.Clear() + # clear cache + $PodeContext.Server.Cache.Items.Clear() + $PodeContext.Server.Cache.Storage.Clear() + # clear up secret vaults/cache Unregister-PodeSecretVaults -ThrowError $PodeContext.Server.Secrets.Vaults.Clear() diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 new file mode 100644 index 000000000..3dce44af4 --- /dev/null +++ b/src/Public/Caching.ps1 @@ -0,0 +1,303 @@ +#TODO: do we need a housekeeping timer? +#TODO: test support for custom storage in get/set + + +function Get-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null, + + [switch] + $Metadata + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + return (Get-PodeCacheInternal -Name $Name -Metadata:$Metadata) + } + + # used custom storage + if (Test-PodeCacheStorage -Name $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Name, $Metadata.IsPresent) -Splat -Return) + } + + # storage not found! + throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Name)'" +} + +function Set-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $InputObject, + + [Parameter()] + [int] + $Ttl = 0, + + [Parameter()] + [string] + $Storage = $null + ) + + # use the global settable default here + if ($Ttl -le 0) { + $Ttl = $PodeContext.Server.Cache.DefaultTtl + } + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Set-PodeCacheInternal -Name $Name -InputObject $InputObject -Ttl $Ttl + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $Value, $Ttl) -Splat + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Name)'" + } +} + +function Test-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + return (Test-PodeCacheInternal -Name $Name) + } + + # used custom storage + if (Test-PodeCacheStorage -Name $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Name) -Splat -Return) + } + + # storage not found! + throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Name)' exists" +} + +function Remove-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Remove-PodeCacheInternal -Name $Name + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Name) -Splat + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Name)'" + } +} + +function Clear-PodeCache { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Clear-PodeCacheInternal + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to clear cached" + } +} + +function Add-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Get, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Set, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Remove, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Test, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Clear, + + [switch] + $Default + ) + + # test if storage already exists + if (Test-PodeCacheStorage -Name $Name) { + throw "Cache Storage with name '$($Name) already exists" + } + + # add cache storage + $PodeContext.Server.Cache.Storage[$Name] = @{ + Name = $Name + Get = $Get + Set = $Set + Remove = $Remove + Test = $Test + Clear = $Clear + Default = $Default.IsPresent + } + + # is default storage? + if ($Default) { + $PodeContext.Server.Cache.DefaultStorage = $Name + } +} + +function Remove-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $null = $PodeContext.Server.Cache.Storage.Remove($Name) +} + +function Get-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.Storage[$Name] +} + +function Test-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.ContainsKey($Name) +} + +function Set-PodeCacheDefaultStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $PodeContext.Server.Cache.DefaultStorage = $Name +} + +function Get-PodeCacheDefaultStorage { + [CmdletBinding()] + param() + + return $PodeContext.Server.Cache.DefaultStorage +} + +function Set-PodeCacheDefaultTtl { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [int] + $Value + ) + + if ($Value -le 0) { + return + } + + $PodeContext.Server.Cache.DefaultTtl = $Value +} + +function Get-PodeCacheDefaultTtl { + [CmdletBinding()] + param() + + return $PodeContext.Server.Cache.DefaultTtl +} \ No newline at end of file From edbb93f0ec576568b630b50db7dd7718ada464c3 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Thu, 23 Nov 2023 22:28:44 +0000 Subject: [PATCH 2/5] #1184: add cache housekeeper, fix custom storage support --- examples/caching.ps1 | 34 ++++++++++- src/Private/Caching.ps1 | 38 +++++++++++- src/Private/Context.ps1 | 1 + src/Private/Server.ps1 | 3 + src/Public/Caching.ps1 | 8 +-- tests/unit/Server.Tests.ps1 | 112 +++++++++++++++++++----------------- 6 files changed, 132 insertions(+), 64 deletions(-) diff --git a/examples/caching.ps1 b/examples/caching.ps1 index 423188418..26ad157a1 100644 --- a/examples/caching.ps1 +++ b/examples/caching.ps1 @@ -15,6 +15,34 @@ Start-PodeServer -Threads 3 { Set-PodeCacheDefaultTtl -Value 60 + $params = @{ + Set = { + param($name, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $name "$($value)" EX $ttl + } + Get = { + param($name, $metadata) + $result = redis-cli -h localhost -p 6379 GET $name + $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { + return $null + } + return $result + } + Test = { + param($name) + $result = redis-cli -h localhost -p 6379 EXISTS $name + return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + } + Remove = { + param($name) + $null = redis-cli -h localhost -p 6379 EXPIRE $name -1 + } + Clear = {} + } + Add-PodeCacheStorage -Name 'Redis' @params + + # get cpu, and cache it Add-PodeRoute -Method Get -Path '/' -ScriptBlock { if ($null -ne $cache:cpu) { @@ -23,8 +51,12 @@ Start-PodeServer -Threads 3 { return } - $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + # $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Start-Sleep -Milliseconds 500 + $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # $cpu = (Get-Random -Minimum 1 -Maximum 1000) + # Write-PodeJsonResponse -Value @{ CPU = $cpu } # Write-PodeHost 'here - raw' } diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 index d5e702b3a..b7bab08b7 100644 --- a/src/Private/Caching.ps1 +++ b/src/Private/Caching.ps1 @@ -15,7 +15,7 @@ function Get-PodeCacheInternal { # check ttl/expiry if ($meta.Expiry -lt [datetime]::UtcNow) { - Remove-PodeCache -Name $Name + Remove-PodeCacheInternal -Name $Name return $null } @@ -68,9 +68,41 @@ function Remove-PodeCacheInternal { $Name ) - $null = $PodeContext.Server.Cache.Items.Remove($Name) + Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { + $null = $PodeContext.Server.Cache.Items.Remove($Name) + } } function Clear-PodeCacheInternal { - $null = $PodeContext.Server.Cache.Items.Clear() + Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { + $null = $PodeContext.Server.Cache.Items.Clear() + } +} + +function Start-PodeCacheHousekeeper { + if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage))) { + return + } + + Add-PodeTimer -Name '__pode_cache_housekeeper__' -Interval 10 -ScriptBlock { + $keys = Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -Return -ScriptBlock { + if ($PodeContext.Server.Cache.Items.Count -eq 0) { + return + } + + return $PodeContext.Server.Cache.Items.Keys.Clone() + } + + if (Test-PodeIsEmpty $keys) { + return + } + + $now = [datetime]::UtcNow + + foreach ($key in $keys) { + if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) { + Remove-PodeCacheInternal -Name $key + } + } + } } \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index d5ac517fd..73e855bf6 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -404,6 +404,7 @@ function New-PodeContext { # threading locks, etc. $ctx.Threading.Lockables = @{ Global = [hashtable]::Synchronized(@{}) + Cache = [hashtable]::Synchronized(@{}) Custom = @{} } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 4c7b0fcfb..4dd58d881 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -41,6 +41,9 @@ function Start-PodeInternalServer { # start timer for task housekeeping Start-PodeTaskHousekeeper + # start the cache housekeeper + Start-PodeCacheHousekeeper + # create timer/schedules for auto-restarting New-PodeAutoRestartServer diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index 3dce44af4..144992679 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -1,7 +1,3 @@ -#TODO: do we need a housekeeping timer? -#TODO: test support for custom storage in get/set - - function Get-PodeCache { [CmdletBinding()] param( @@ -73,7 +69,7 @@ function Set-PodeCache { # used custom storage elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $Value, $Ttl) -Splat + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $InputObject, $Ttl) -Splat } # storage not found! @@ -259,7 +255,7 @@ function Test-PodeCacheStorage { $Name ) - return $PodeContext.Server.Cache.ContainsKey($Name) + return $PodeContext.Server.Cache.Storage.ContainsKey($Name) } function Set-PodeCacheDefaultStorage { diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 68d207ecb..2906a3eea 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -3,8 +3,8 @@ $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } $PodeContext = @{ - Server = $null - Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } + Server = $null + Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } RunspacePools = @{} } @@ -98,121 +98,125 @@ Describe 'Restart-PodeInternalServer' { It 'Resetting the server values' { $PodeContext = @{ - Tokens = @{ + Tokens = @{ Cancellation = New-Object System.Threading.CancellationTokenSource - Restart = New-Object System.Threading.CancellationTokenSource + Restart = New-Object System.Threading.CancellationTokenSource } - Server = @{ - Routes = @{ - GET = @{ 'key' = 'value' } + Server = @{ + Routes = @{ + GET = @{ 'key' = 'value' } POST = @{ 'key' = 'value' } } - Handlers = @{ + Handlers = @{ SMTP = @{} } - Verbs = @{ + Verbs = @{ key = @{} } - Logging = @{ + Logging = @{ Types = @{ 'key' = 'value' } } - Middleware = @{ 'key' = 'value' } - Endpoints = @{ 'key' = 'value' } - EndpointsMap = @{ 'key' = 'value' } - Endware = @{ 'key' = 'value' } - ViewEngine = @{ - Type = 'pode' + Middleware = @{ 'key' = 'value' } + Endpoints = @{ 'key' = 'value' } + EndpointsMap = @{ 'key' = 'value' } + Endware = @{ 'key' = 'value' } + ViewEngine = @{ + Type = 'pode' Extension = 'pode' - Script = $null + Script = $null IsDynamic = $true } - Cookies = @{} - Sessions = @{ 'key' = 'value' } + Cookies = @{} + Sessions = @{ 'key' = 'value' } Authentications = @{ Methods = @{ 'key' = 'value' } } - Authorisations = @{ + Authorisations = @{ Methods = @{ 'key' = 'value' } } - State = @{ 'key' = 'value' } - Output = @{ + State = @{ 'key' = 'value' } + Output = @{ Variables = @{ 'key' = 'value' } } - Configuration = @{ 'key' = 'value' } - Sockets = @{ + Configuration = @{ 'key' = 'value' } + Sockets = @{ Listeners = @() - Queues = @{ + Queues = @{ Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new() } } - Signals = @{ + Signals = @{ Listeners = @() - Queues = @{ - Sockets = @{} + Queues = @{ + Sockets = @{} Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new() } } - OpenAPI = @{} - BodyParsers = @{} - AutoImport = @{ - Modules = @{ Exported = @() } - Snapins = @{ Exported = @() } - Functions = @{ Exported = @() } - SecretVaults = @{ + OpenAPI = @{} + BodyParsers = @{} + AutoImport = @{ + Modules = @{ Exported = @() } + Snapins = @{ Exported = @() } + Functions = @{ Exported = @() } + SecretVaults = @{ SecretManagement = @{ Exported = @() } } } - Views = @{ 'key' = 'value' } - Events = @{ + Views = @{ 'key' = 'value' } + Events = @{ Start = @{} } - Modules = @{} - Security = @{ + Modules = @{} + Security = @{ Headers = @{} - Cache = @{ - ContentSecurity = @{} + Cache = @{ + ContentSecurity = @{} PermissionsPolicy = @{} } } - Secrets = @{ + Secrets = @{ Vaults = @{} - Keys = @{} + Keys = @{} + } + Cache = @{ + Items = @{} + Storage = @{} } } - Metrics = @{ + Metrics = @{ Server = @{ RestartCount = 0 } } - Timers = @{ + Timers = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } } Schedules = @{ - Enabled = $true - Items = @{ + Enabled = $true + Items = @{ key = 'value' } Processes = @{} } - Tasks = @{ + Tasks = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } Results = @{} } - Fim = @{ + Fim = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } } Threading = @{ - Lockables = @{ Custom = @{} } - Mutexes = @{} + Lockables = @{ Custom = @{} } + Mutexes = @{} Semaphores = @{} } } From b6b3285133700303233b7861d677cc0e026a666a Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 28 Nov 2023 22:39:01 +0000 Subject: [PATCH 3/5] #1184: add function summaries, and caching docs --- docs/Tutorials/Caching.md | 153 ++++++++++++++++++++ examples/caching.ps1 | 18 +-- src/Private/Caching.ps1 | 35 +++-- src/Private/Helpers.ps1 | 4 +- src/Public/Caching.ps1 | 277 +++++++++++++++++++++++++++++++++--- tests/unit/Server.Tests.ps1 | 1 + 6 files changed, 446 insertions(+), 42 deletions(-) create mode 100644 docs/Tutorials/Caching.md diff --git a/docs/Tutorials/Caching.md b/docs/Tutorials/Caching.md new file mode 100644 index 000000000..cf7f0338c --- /dev/null +++ b/docs/Tutorials/Caching.md @@ -0,0 +1,153 @@ +# Caching + +Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also setup custom caching storage solutions - such as Redis, and others. + +The default TTL for cached items is 3,600 seconds (1 hour), and this value can be customised either globally or per item. There is also a `$cache:` scoped variable available for use. + +## Caching Items + +To add an item into the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. + +For example, the following would retrieve the current CPU on Windows machines and cache it for 60 seconds: + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = Get-PodeCache -Key 'cpu' + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 60s + $cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + $cpu | Set-PodeCache -Key 'cpu' -Ttl 60 + + Write-PodeJsonResponse -Value @{ CPU = $cpu } +} +``` + +Alternatively, you could use the `$cache:` scoped variable instead. However, using this there is no way to pass the TTL when setting new cached items, so all items cached in this manner will use the default TTL (1 hour, unless changed). Changing the default TTL is discussed [below](#default-ttl). + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = $cache:cpu + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 1hr + $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } +} +``` + +You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, you can see if the key actually does exist. + +If you need to invalidate a cached value you can use [`Remove-PodeCache`](../../Functions/Caching/Remove-PodeCache), or if you need to invalidate the whole cache you can use [`Clear-PodeCache`](../../Functions/Caching/Clear-PodeCache). + +### Default TTL + +The default TTL for cached items, when the server starts, is 1 hour. This can be changed by using [`Set-PodeCacheDefaultTtl`](../../Functions/Caching/Set-PodeCacheDefaultTtl). The following updates the default TTL to 60 seconds: + +```powershell +Start-PodeServer { + Set-PodeCacheDefaultTtl -Value 60 +} +``` + +All new cached items will use this TTL by default, unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. + +## Custom Storage + +The inbuilt storage used by Pode is a simple in-memory synchronized hashtable, if you're running multiple instances of your Pode server then you'll have multiple caches as well - potentially with different values for the keys. + +You can setup custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also setup multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. + +When setting up a new cache storage, you are required to specific a series of scriptblocks for: + +* Setting a cached item (create/update). (`-Set`) +* Getting a cached item's value. (`-Get`) +* Testing if a cached item exists. (`-Test`) +* Removing a cached item. (`-Remove`) +* Clearing a cache of all items. (`-Clear`) + +!!! note + Not all providers will support all options, such as clearing the whole cache. When this is the case simply pass an empty scriptblock to the parameter. + +The `-Test` and `-Remove` scriptblocks will each be supplied the key for cached item; the `-Test` scriptblock should return a boolea value. The `-Set` scriptblock will be supplied the key, value and TTL for the cached item. The `-Get` scriptblock will be supplied the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata is flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. + +For example, say you want to use Redis to store your cached items, then you would have a similar setup to the below: + +```powershell +$params = @{ + Set = { + param($key, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl + } + Get = { + param($key, $metadata) + $result = redis-cli -h localhost -p 6379 GET $key + $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { + return $null + } + + if ($metadata) { + $ttl = redis-cli -h localhost -p 6379 TTL $key + $ttl = [int]([System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText')) + + $result = @{ + Value = $result + Ttl = $ttl + Expiry = [datetime]::UtcNow.AddSeconds($ttl) + } + } + + return $result + } + Test = { + param($key) + $result = redis-cli -h localhost -p 6379 EXISTS $key + return ([System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') -eq '1') + } + Remove = { + param($key) + $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 + } + Clear = {} +} + +Add-PodeCacheStorage -Name 'Redis' @params +``` + +And then to use the storage, pass the name to the `-Storage` parameter: + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = Get-PodeCache -Key 'cpu' -Storage 'Redis' + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 60s + $cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + $cpu | Set-PodeCache -Key 'cpu' -Ttl 60 -Storage 'Redis' + + Write-PodeJsonResponse -Value @{ CPU = $cpu } +} +``` + +### Default Storage + +Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. + +```powershell +Start-PodeServer { + Set-PodeCacheDefaultStorage -Name 'Redis' +} +``` diff --git a/examples/caching.ps1 b/examples/caching.ps1 index 26ad157a1..d2dfda012 100644 --- a/examples/caching.ps1 +++ b/examples/caching.ps1 @@ -17,12 +17,12 @@ Start-PodeServer -Threads 3 { $params = @{ Set = { - param($name, $value, $ttl) - $null = redis-cli -h localhost -p 6379 SET $name "$($value)" EX $ttl + param($key, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl } Get = { - param($name, $metadata) - $result = redis-cli -h localhost -p 6379 GET $name + param($key, $metadata) + $result = redis-cli -h localhost -p 6379 GET $key $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { return $null @@ -30,13 +30,13 @@ Start-PodeServer -Threads 3 { return $result } Test = { - param($name) - $result = redis-cli -h localhost -p 6379 EXISTS $name + param($key) + $result = redis-cli -h localhost -p 6379 EXISTS $key return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') } Remove = { - param($name) - $null = redis-cli -h localhost -p 6379 EXPIRE $name -1 + param($key) + $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 } Clear = {} } @@ -45,7 +45,7 @@ Start-PodeServer -Threads 3 { # get cpu, and cache it Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - if ($null -ne $cache:cpu) { + if ((Test-PodeCache -Key 'cpu') -and ($null -ne $cache:cpu)) { Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } # Write-PodeHost 'here - cached' return diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 index b7bab08b7..30c76e50b 100644 --- a/src/Private/Caching.ps1 +++ b/src/Private/Caching.ps1 @@ -2,20 +2,20 @@ function Get-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [switch] $Metadata ) - $meta = $PodeContext.Server.Cache.Items[$Name] + $meta = $PodeContext.Server.Cache.Items[$Key] if ($null -eq $meta) { return $null } # check ttl/expiry if ($meta.Expiry -lt [datetime]::UtcNow) { - Remove-PodeCacheInternal -Name $Name + Remove-PodeCacheInternal -Key $Key return $null } @@ -32,7 +32,7 @@ function Set-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter(Mandatory = $true)] [object] @@ -44,7 +44,7 @@ function Set-PodeCacheInternal { ) # crete (or update) value value - $PodeContext.Server.Cache.Items[$Name] = @{ + $PodeContext.Server.Cache.Items[$Key] = @{ Value = $InputObject Ttl = $Ttl Expiry = [datetime]::UtcNow.AddSeconds($Ttl) @@ -55,21 +55,36 @@ function Test-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name + $Key ) - return $PodeContext.Server.Cache.Items.ContainsKey($Name) + # if it's not in the cache at all, return false + if (!$PodeContext.Server.Cache.Items.ContainsKey($Key)) { + return $false + } + + # fetch the items metadata, and check expiry. If it's expired return false. + $meta = $PodeContext.Server.Cache.Items[$Key] + + # check ttl/expiry + if ($meta.Expiry -lt [datetime]::UtcNow) { + Remove-PodeCacheInternal -Key $Key + return $false + } + + # it exists, and isn't expired + return $true } function Remove-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name + $Key ) Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { - $null = $PodeContext.Server.Cache.Items.Remove($Name) + $null = $PodeContext.Server.Cache.Items.Remove($Key) } } @@ -101,7 +116,7 @@ function Start-PodeCacheHousekeeper { foreach ($key in $keys) { if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) { - Remove-PodeCacheInternal -Name $key + Remove-PodeCacheInternal -Key $key } } } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index a2fa9c23c..9663380c4 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2564,12 +2564,12 @@ function Invoke-PodeCacheScriptConversion { while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+)\s*=)') { $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Name '$($Matches['name'])' -InputObject ") + $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Key '$($Matches['name'])' -InputObject ") } while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+))') { $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Name '$($Matches['name'])')") + $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Key '$($Matches['name'])')") } if ($found) { diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index 144992679..df7972b19 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -1,9 +1,37 @@ +<# +.SYNOPSIS +Return the value of a key from the cache. You can use "$value = $cache:key" as well. + +.DESCRIPTION +Return the value of a key from the cache, or returns the value plus metadata such as expiry time if required. You can use "$value = $cache:key" as well. + +.PARAMETER Key +The Key to be retrieved. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.PARAMETER Metadata +If supplied, and if supported by the cache storage, an metadata such as expiry times will also be returned. + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' -Metadata + +.EXAMPLE +$value = $cache:ExampleKey +#> function Get-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -20,24 +48,61 @@ function Get-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - return (Get-PodeCacheInternal -Name $Name -Metadata:$Metadata) + return (Get-PodeCacheInternal -Key $Key -Metadata:$Metadata) } # used custom storage - if (Test-PodeCacheStorage -Name $Storage) { - return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Name, $Metadata.IsPresent) -Splat -Return) + if (Test-PodeCacheStorage -Key $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Key, $Metadata.IsPresent) -Splat -Return) } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Key)'" } +<# +.SYNOPSIS +Set (create/update) a key in the cache. You can use "$cache:key = 'value'" as well. + +.DESCRIPTION +Set (create/update) a key in the cache, with an optional TTL value. You can use "$cache:key = 'value'" as well. + +.PARAMETER Key +The Key to be set. + +.PARAMETER InputObject +The value of the key to be set, can be any object type. + +.PARAMETER Ttl +An optional TTL value, in seconds. The default is whatever "Get-PodeCacheDefaultTtl" retuns, which will be 3600 seconds when not set. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Storage 'ExampleStorage' + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Ttl 300 + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject @{ Value = 'ExampleValue' } + +.EXAMPLE +@{ Value = 'ExampleValue' } | Set-PodeCache -Key 'ExampleKey' + +.EXAMPLE +$cache:ExampleKey = 'ExampleValue' +#> function Set-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] @@ -64,26 +129,45 @@ function Set-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - Set-PodeCacheInternal -Name $Name -InputObject $InputObject -Ttl $Ttl + Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $InputObject, $Ttl) -Splat + elseif (Test-PodeCacheStorage -Key $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat } # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Key)'" } } +<# +.SYNOPSIS +Test if a key exists in the cache. + +.DESCRIPTION +Test if a key exists in the cache, and isn't expired. + +.PARAMETER Key +The Key to test. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Test-PodeCache -Key 'ExampleKey' + +.EXAMPLE +Test-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' +#> function Test-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -97,24 +181,43 @@ function Test-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - return (Test-PodeCacheInternal -Name $Name) + return (Test-PodeCacheInternal -Key $Key) } # used custom storage - if (Test-PodeCacheStorage -Name $Storage) { - return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Name) -Splat -Return) + if (Test-PodeCacheStorage -Key $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Key) -Splat -Return) } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Name)' exists" + throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Key)' exists" } +<# +.SYNOPSIS +Remove a key from the cache. + +.DESCRIPTION +Remove a key from the cache. + +.PARAMETER Key +The Key to be removed. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Remove-PodeCache -Key 'ExampleKey' + +.EXAMPLE +Remove-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' +#> function Remove-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -128,20 +231,36 @@ function Remove-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - Remove-PodeCacheInternal -Name $Name + Remove-PodeCacheInternal -Key $Key } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Name) -Splat + elseif (Test-PodeCacheStorage -Key $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat } # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Key)'" } } +<# +.SYNOPSIS +Clear all keys from the cache. + +.DESCRIPTION +Clear all keys from the cache. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Clear-PodeCache + +.EXAMPLE +Clear-PodeCache -Storage 'ExampleStorage' +#> function Clear-PodeCache { [CmdletBinding()] param( @@ -161,7 +280,7 @@ function Clear-PodeCache { } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { + elseif (Test-PodeCacheStorage -Key $Storage) { Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear } @@ -171,6 +290,37 @@ function Clear-PodeCache { } } +<# +.SYNOPSIS +Add a cache storage. + +.DESCRIPTION +Add a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.PARAMETER Get +A Get ScriptBlock, to retrieve a key's value from the cache, or the value plus metadata if required. Supplied parameters: Key, Metadata. + +.PARAMETER Set +A Set ScriptBlock, to set/create/update a key's value in the cache. Supplied parameters: Key, Value, TTL. + +.PARAMETER Remove +A Remove ScriptBlock, to remove a key from the cache. Supplied parameters: Key. + +.PARAMETER Test +A Test ScriptBlock, to test if a key exists in the cache. Supplied parameters: Key. + +.PARAMETER Clear +A Clear ScriptBlock, to remove all keys from the cache. Use an empty ScriptBlock if not supported. + +.PARAMETER Default +If supplied, this cache storage will be set as the default storage. + +.EXAMPLE +Add-PodeCacheStorage -Name 'ExampleStorage' -Get {} -Set {} -Remove {} -Test {} -Clear {} +#> function Add-PodeCacheStorage { [CmdletBinding()] param( @@ -225,6 +375,19 @@ function Add-PodeCacheStorage { } } +<# +.SYNOPSIS +Remove a cache storage. + +.DESCRIPTION +Remove a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +Remove-PodeCacheStorage -Name 'ExampleStorage' +#> function Remove-PodeCacheStorage { [CmdletBinding()] param( @@ -236,6 +399,19 @@ function Remove-PodeCacheStorage { $null = $PodeContext.Server.Cache.Storage.Remove($Name) } +<# +.SYNOPSIS +Returns a cache storage. + +.DESCRIPTION +Returns a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +$storage = Get-PodeCacheStorage -Name 'ExampleStorage' +#> function Get-PodeCacheStorage { [CmdletBinding()] param( @@ -247,6 +423,19 @@ function Get-PodeCacheStorage { return $PodeContext.Server.Cache.Storage[$Name] } +<# +.SYNOPSIS +Test if a cache storage has been added/exists. + +.DESCRIPTION +Test if a cache storage has been added/exists. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +if (Test-PodeCacheStorage -Name 'ExampleStorage') { } +#> function Test-PodeCacheStorage { [CmdletBinding()] param( @@ -258,6 +447,19 @@ function Test-PodeCacheStorage { return $PodeContext.Server.Cache.Storage.ContainsKey($Name) } +<# +.SYNOPSIS +Set a default cache storage. + +.DESCRIPTION +Set a default cache storage. + +.PARAMETER Name +The Name of the default storage to use for caching. + +.EXAMPLE +Set-PodeCacheDefaultStorage -Name 'ExampleStorage' +#> function Set-PodeCacheDefaultStorage { [CmdletBinding()] param( @@ -269,6 +471,16 @@ function Set-PodeCacheDefaultStorage { $PodeContext.Server.Cache.DefaultStorage = $Name } +<# +.SYNOPSIS +Returns the current default cache Storage name. + +.DESCRIPTION +Returns the current default cache Storage name. Empty/null if one isn't set. + +.EXAMPLE +$storageName = Get-PodeCacheDefaultStorage +#> function Get-PodeCacheDefaultStorage { [CmdletBinding()] param() @@ -276,6 +488,19 @@ function Get-PodeCacheDefaultStorage { return $PodeContext.Server.Cache.DefaultStorage } +<# +.SYNOPSIS +Set a default cache TTL. + +.DESCRIPTION +Set a default cache TTL. + +.PARAMETER Value +A default TTL value, in seconds, to use when setting cache key expiries. + +.EXAMPLE +Set-PodeCacheDefaultTtl -Value 3600 +#> function Set-PodeCacheDefaultTtl { [CmdletBinding()] param( @@ -291,6 +516,16 @@ function Set-PodeCacheDefaultTtl { $PodeContext.Server.Cache.DefaultTtl = $Value } +<# +.SYNOPSIS +Returns the current default cache TTL value. + +.DESCRIPTION +Returns the current default cache TTL value. 3600 seconds is the default TTL if not set. + +.EXAMPLE +$ttl = Get-PodeCacheDefaultTtl +#> function Get-PodeCacheDefaultTtl { [CmdletBinding()] param() diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 2906a3eea..f9da5c770 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -26,6 +26,7 @@ Describe 'Start-PodeInternalServer' { Mock Import-PodeModulesIntoRunspaceState { } Mock Import-PodeSnapinsIntoRunspaceState { } Mock Import-PodeFunctionsIntoRunspaceState { } + Mock Start-PodeCacheHousekeeper { } Mock Invoke-PodeEvent { } Mock Write-Verbose { } From e2b112a2df061ecde61e20be72ada0764f1d3f1f Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 10 Dec 2023 22:47:39 +0000 Subject: [PATCH 4/5] #1184: tweaks for caching docs --- docs/Tutorials/Caching.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Tutorials/Caching.md b/docs/Tutorials/Caching.md index cf7f0338c..3c5904b30 100644 --- a/docs/Tutorials/Caching.md +++ b/docs/Tutorials/Caching.md @@ -1,12 +1,12 @@ # Caching -Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also setup custom caching storage solutions - such as Redis, and others. +Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also set up custom caching storage solutions - such as Redis, and others. The default TTL for cached items is 3,600 seconds (1 hour), and this value can be customised either globally or per item. There is also a `$cache:` scoped variable available for use. ## Caching Items -To add an item into the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. +To add an item to the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. For example, the following would retrieve the current CPU on Windows machines and cache it for 60 seconds: @@ -44,7 +44,7 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { } ``` -You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, you can see if the key actually does exist. +You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, so you can see if the key does exist. If you need to invalidate a cached value you can use [`Remove-PodeCache`](../../Functions/Caching/Remove-PodeCache), or if you need to invalidate the whole cache you can use [`Clear-PodeCache`](../../Functions/Caching/Clear-PodeCache). @@ -58,13 +58,13 @@ Start-PodeServer { } ``` -All new cached items will use this TTL by default, unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. +All new cached items will use this TTL by default unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. ## Custom Storage The inbuilt storage used by Pode is a simple in-memory synchronized hashtable, if you're running multiple instances of your Pode server then you'll have multiple caches as well - potentially with different values for the keys. -You can setup custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also setup multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. +You can set up custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also set up multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. When setting up a new cache storage, you are required to specific a series of scriptblocks for: @@ -77,9 +77,9 @@ When setting up a new cache storage, you are required to specific a series of sc !!! note Not all providers will support all options, such as clearing the whole cache. When this is the case simply pass an empty scriptblock to the parameter. -The `-Test` and `-Remove` scriptblocks will each be supplied the key for cached item; the `-Test` scriptblock should return a boolea value. The `-Set` scriptblock will be supplied the key, value and TTL for the cached item. The `-Get` scriptblock will be supplied the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata is flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. +The `-Test` and `-Remove` scriptblocks will each be supplied the key for the cached item; the `-Test` scriptblock should return a boolean value. The `-Set` scriptblock will be supplied with the key, value, and TTL for the cached item. The `-Get` scriptblock will be supplied with the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. -For example, say you want to use Redis to store your cached items, then you would have a similar setup to the below: +For example, say you want to use Redis to store your cached items, then you would have a similar setup to the one below. ```powershell $params = @{ @@ -123,7 +123,7 @@ $params = @{ Add-PodeCacheStorage -Name 'Redis' @params ``` -And then to use the storage, pass the name to the `-Storage` parameter: +Then to use the storage, pass the name to the `-Storage` parameter: ```powershell Add-PodeRoute -Method Get -Path '/' -ScriptBlock { @@ -144,7 +144,7 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { ### Default Storage -Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. +Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom-added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. ```powershell Start-PodeServer { From 93a2ed85aa185c2637cd069dcaa1fd0b72c5b04a Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 10 Dec 2023 22:59:31 +0000 Subject: [PATCH 5/5] #1184: docs styling fix --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 14f0c4acf..201ca92d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,8 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Support for dynamically building Routes from Functions and Modules * Generate/bind self-signed certificates * Secret management support to load secrets from vaults -* Support for File Watchers* In-memory caching, with optional support for external providers (such as Redis) +* Support for File Watchers +* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 🏢 Companies using Pode