Skip to content

Commit

Permalink
Merge pull request #1197 from Badgerati/Issue-1184
Browse files Browse the repository at this point in the history
Adds inbuilt support for caching values
  • Loading branch information
Badgerati authored Dec 13, 2023
2 parents 7459f73 + 93a2ed8 commit 9c71d26
Show file tree
Hide file tree
Showing 11 changed files with 1,004 additions and 56 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 153 additions & 0 deletions docs/Tutorials/Caching.md
Original file line number Diff line number Diff line change
@@ -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 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 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:

```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`, 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).

### 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 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:

* 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 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 one 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
```

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'
}
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We
* 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

## 🏢 Companies using Pode
Expand Down
63 changes: 63 additions & 0 deletions examples/caching.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
$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

$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
}
return $result
}
Test = {
param($key)
$result = redis-cli -h localhost -p 6379 EXISTS $key
return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText')
}
Remove = {
param($key)
$null = redis-cli -h localhost -p 6379 EXPIRE $key -1
}
Clear = {}
}
Add-PodeCacheStorage -Name 'Redis' @params


# get cpu, and cache it
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
if ((Test-PodeCache -Key 'cpu') -and ($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
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'
}

}
17 changes: 16 additions & 1 deletion src/Pode.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
123 changes: 123 additions & 0 deletions src/Private/Caching.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
function Get-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key,

[switch]
$Metadata
)

$meta = $PodeContext.Server.Cache.Items[$Key]
if ($null -eq $meta) {
return $null
}

# check ttl/expiry
if ($meta.Expiry -lt [datetime]::UtcNow) {
Remove-PodeCacheInternal -Key $Key
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]
$Key,

[Parameter(Mandatory = $true)]
[object]
$InputObject,

[Parameter()]
[int]
$Ttl = 0
)

# crete (or update) value value
$PodeContext.Server.Cache.Items[$Key] = @{
Value = $InputObject
Ttl = $Ttl
Expiry = [datetime]::UtcNow.AddSeconds($Ttl)
}
}

function Test-PodeCacheInternal {
param(
[Parameter(Mandatory = $true)]
[string]
$Key
)

# 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]
$Key
)

Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock {
$null = $PodeContext.Server.Cache.Items.Remove($Key)
}
}

function Clear-PodeCacheInternal {
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 -Key $key
}
}
}
}
Loading

0 comments on commit 9c71d26

Please sign in to comment.