Shared State Snapshots

Capture and restore a shared in-memory object with Export-KrSharedState, Import-KrSharedState, and Use-KrLock.

Prerequisites: see Introduction.

Full source

File: pwsh/tutorial/examples/4.3-Shared-State-Snapshots.ps1

<#
    Sample Kestrun server demonstrating shared-state snapshot export/import.
    This example shows how to snapshot and restore a thread-safe shared
    state object while using a named lock for grouped updates.
    FileName: 4.3-Shared-State-Snapshots.ps1
#>

param(
    [int]$Port = $env:PORT ?? 5000
)

Initialize-KrRoot -Path $PSScriptRoot

New-KrServer -Name 'Shared State Snapshot Server'
Add-KrEndpoint -Port $Port

$snapshotLockKey = 'tutorial:shared-state-snapshot'

Set-KrSharedState -Name 'AppState' -Value @{
    VisitCount = 0
    Notes = @()
    LastUpdated = (Get-Date).ToUniversalTime()
} -ThreadSafe

Enable-KrConfiguration

Add-KrMapRoute -Verbs Get -Pattern '/state' -ScriptBlock {
    $response = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'

        @{
            visitCount = [int]$state.VisitCount
            notes = @($state.Notes)
            lastUpdated = [datetime]$state.LastUpdated
            snapshotLockKey = $snapshotLockKey
        }
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

Add-KrMapRoute -Verbs Post -Pattern '/visit' -ScriptBlock {
    $response = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'
        $null = ($state.VisitCount = [int]$state.VisitCount + 1)
        $null = ($state.LastUpdated = (Get-Date).ToUniversalTime())

        return @{
            message = 'Visit recorded'
            visitCount = [int]$state.VisitCount
            notes = @($state.Notes)
        }
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

Add-KrMapRoute -Verbs Post -Pattern '/note' -ScriptBlock {
    $body = Get-KrRequestBody
    $note = [string]$body.note

    if ([string]::IsNullOrWhiteSpace($note)) {
        Write-KrJsonResponse -InputObject @{ error = 'note is required' } -StatusCode 400
        return
    }

    $response = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'
        $null = ($state.Notes = @($state.Notes) + $note)
        $null = ($state.LastUpdated = (Get-Date).ToUniversalTime())

        return @{
            message = 'Note added'
            visitCount = [int]$state.VisitCount
            notes = @($state.Notes)
        }
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

Add-KrMapRoute -Verbs Post -Pattern '/snapshot/export' -ScriptBlock {
    $snapshotData = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'
        $snapshotState = [pscustomobject]@{
            VisitCount = [int]$state.VisitCount
            Notes = @($state.Notes)
            LastUpdated = [datetime]$state.LastUpdated
        }

        @{
            snapshot = Export-KrSharedState -InputObject $snapshotState
            visitCount = $snapshotState.VisitCount
            noteCount = @($snapshotState.Notes).Count
        }
    }

    Write-KrJsonResponse -InputObject @{
        snapshot = $snapshotData.snapshot
        visitCount = $snapshotData.visitCount
        noteCount = $snapshotData.noteCount
        exportedAt = (Get-Date).ToUniversalTime()
    } -StatusCode 200
}

Add-KrMapRoute -Verbs Post -Pattern '/snapshot/reset' -ScriptBlock {
    $response = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'
        $null = ($state.VisitCount = 0)
        $null = ($state.Notes = @())
        $null = ($state.LastUpdated = (Get-Date).ToUniversalTime())

        return @{
            message = 'State reset'
            visitCount = 0
            notes = @()
        }
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

Add-KrMapRoute -Verbs Post -Pattern '/snapshot/import' -ScriptBlock {
    $body = Get-KrRequestBody
    $snapshot = [string]$body.snapshot

    if ([string]::IsNullOrWhiteSpace($snapshot)) {
        Write-KrJsonResponse -InputObject @{ error = 'snapshot is required' } -StatusCode 400
        return
    }

    $restored = Import-KrSharedState -InputString $snapshot
    $response = Use-KrLock -Key $snapshotLockKey -ScriptBlock {
        $state = Get-KrSharedState -Name 'AppState'
        $null = ($state.VisitCount = [int]$restored.VisitCount)
        $null = ($state.Notes = if ($null -eq $restored.Notes) { @() } else { @($restored.Notes) })
        $null = ($state.LastUpdated = [datetime]$restored.LastUpdated)

        return @{
            message = 'Snapshot restored'
            visitCount = [int]$state.VisitCount
            notes = @($state.Notes)
        }
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

Start-KrServer

Step-by-step

  1. Initialize root: Initialize-KrRoot -Path $PSScriptRoot keeps file resolution and generated content predictable.
  2. Build the host: New-KrServer and Add-KrEndpoint expose a simple API on the selected port.
  3. Define one lock key: $snapshotLockKey = 'tutorial:shared-state-snapshot' gives every grouped update the same synchronization boundary.
  4. Seed the shared object: Set-KrSharedState -Name 'AppState' -Value @{ ... } -ThreadSafe creates a mutable document with VisitCount, Notes, and LastUpdated.
  5. Keep critical sections small: /state, /visit, /note, and /snapshot/reset use Use-KrLock only while reading or mutating the shared object, then write the HTTP response after the lock is released.
  6. Export a snapshot: /snapshot/export takes a copy of the current state inside the lock, then calls Export-KrSharedState to return the serialized XML snapshot.
  7. Restore a snapshot: /snapshot/import validates the request body, deserializes the snapshot with Import-KrSharedState, then applies the restored values inside the same named lock.
  8. Verify the round trip: modify the state, export it, reset it, then import the snapshot and confirm the original values return.

Behavior contract

  • Inputs: POST /visit, POST /note, POST /snapshot/export, POST /snapshot/reset, and POST /snapshot/import.
  • Outputs: JSON responses with the current visit count, note list, and serialized snapshot payload.
  • Synchronization: the same named lock key protects consistent reads and grouped updates to the shared AppState object.

Why this stays simple

This tutorial follows the same style as the rest of the PowerShell samples: shared state is created with Set-KrSharedState, routes use public cmdlets, and the locking story stays at the PowerShell level.

  • Use Set-KrSharedState for the shared document.
  • Use Use-KrLock when a route needs to read or update multiple properties together.
  • Use Export-KrSharedState and Import-KrSharedState for snapshot serialization.

That matches the guidance in the shared-state guide: atomic helpers are great for single counters, but named locks are clearer when a route updates several related fields like VisitCount, Notes, and LastUpdated together.

Why not increment directly?

This sample intentionally uses a named lock instead of a raw increment because each write updates more than one property.

Use-KrLock -Key $SnapshotLockKey -ScriptBlock {
    $state = Get-KrSharedState -Name 'AppState'
    $state.VisitCount = [int]$state.VisitCount + 1
    $state.LastUpdated = (Get-Date).ToUniversalTime()
}

That gives you one clear critical section instead of mixing manual locking, direct registry calls, and custom helper functions.

Try it

Run the sample:

pwsh .\docs\pwsh\tutorial\examples\4.3-Shared-State-Snapshots.ps1

Then exercise the snapshot flow.

1. Seed some state

PowerShell:

Invoke-RestMethod http://127.0.0.1:5000/visit -Method Post
Invoke-RestMethod http://127.0.0.1:5000/visit -Method Post
Invoke-RestMethod http://127.0.0.1:5000/note -Method Post -ContentType 'application/json' -Body (@{ note = 'remember helmet' } | ConvertTo-Json)
Invoke-RestMethod http://127.0.0.1:5000/state -Method Get

curl:

curl -X POST http://127.0.0.1:5000/visit
curl -X POST http://127.0.0.1:5000/visit
curl -X POST http://127.0.0.1:5000/note -H 'Content-Type: application/json' -d '{"note":"remember helmet"}'
curl http://127.0.0.1:5000/state

Expected state:

{
  "visitCount": 2,
  "notes": [
    "remember helmet"
  ]
}

2. Export a snapshot, reset, and restore it

$export = Invoke-RestMethod http://127.0.0.1:5000/snapshot/export -Method Post
Invoke-RestMethod http://127.0.0.1:5000/snapshot/reset -Method Post
Invoke-RestMethod http://127.0.0.1:5000/state -Method Get

$body = @{ snapshot = $export.snapshot } | ConvertTo-Json -Depth 5
Invoke-RestMethod http://127.0.0.1:5000/snapshot/import -Method Post -ContentType 'application/json' -Body $body
Invoke-RestMethod http://127.0.0.1:5000/state -Method Get

The first /state call after reset should show zero visits and no notes. The final /state call should restore the exported values.

When to snapshot shared state

  • Persist a small in-memory cache before recycling a host.
  • Capture a support bundle that reproduces a test scenario.
  • Reset a demo environment and then quickly restore it to a known baseline.

If the data must survive process restarts permanently, a database or file-backed store is still the right long-term solution. Snapshot export/import is best for operational handoff and controlled recovery flows.

Troubleshooting

Symptom Likely cause Fix
Import returns 400 Request body does not contain snapshot Send JSON like @{ snapshot = $export.snapshot } \| ConvertTo-Json
Restored state is stale Snapshot was exported before the latest mutation Export a fresh snapshot after the writes you care about
Requests appear serialized The sample uses a single named lock on purpose Keep the critical section small or split state across multiple lock keys

References


Previous / Next

Previous: Shared State Next: Logging