Managing Shared State

Use the Kestrun shared state cmdlets (Set-KrSharedState, Get-KrSharedState, Remove-KrSharedState) to create and manage thread-safe, cross-runspace state accessible from all routes and languages (PowerShell, C#, VB.NET).

Prerequisites: see Introduction.

Full source

File: pwsh/tutorial/examples/4.2-Shared-State.ps1

<#
    Sample Kestrun Server demonstrating shared state management.
    This example shows how to use Set-KrSharedState, Get-KrSharedState, and Remove-KrSharedState cmdlets.
    FileName: 4.2-Shared-State.ps1
#>

param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)

# Initialize Kestrun root directory
Initialize-KrRoot -Path $PSScriptRoot

# Create a new Kestrun server
New-KrServer -Name 'Shared State Server'

# Add a listener using provided parameters
Add-KrEndpoint -Port $Port -IPAddress $IPAddress

# Define shared state using Set-KrSharedState
# This creates thread-safe, cross-runspace accessible state
Set-KrSharedState -Name 'Visits' -Value @{ Count = 0 } -ThreadSafe

Set-KrSharedState -Name 'Config' -Value @{ MaxVisits = 100; AppName = 'SharedStateDemo' } -ThreadSafe
Set-KrSharedState -Name 'StartTime' -Value @{ Time = (Get-Date) } -ThreadSafe

# Enable Kestrun configuration
Enable-KrConfiguration

# Route: GET /info - Show configuration from shared state
Add-KrMapRoute -Verbs Get -Pattern '/info' -ScriptBlock {
    $config = Get-KrSharedState -Name 'Config'
    $startTime = Get-KrSharedState -Name 'StartTime'
    $uptime = (Get-Date) - $startTime.Time

    $response = @{
        appName = $config.AppName
        maxVisits = $config.MaxVisits
        uptimeSeconds = [math]::Round($uptime.TotalSeconds, 2)
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

# Route: GET /visits - Show current visit count
Add-KrMapRoute -Verbs Get -Pattern '/visits' -ScriptBlock {
    $visits = Get-KrSharedState -Name 'Visits'

    Write-KrJsonResponse -InputObject @{
        count = $visits.Count
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    } -StatusCode 200
}

# Route: POST /visit - Increment visit counter
Add-KrMapRoute -Verbs Post -Pattern '/visit' -ScriptBlock {
    $visits = Get-KrSharedState -Name 'Visits'
    $config = Get-KrSharedState -Name 'Config'

    Start-Sleep -Seconds 2  # Simulate some processing delay

    # Atomic increment
    Update-KrSynchronizedCounter -Table $visits -Key 'Count' -By 1

    $response = @{
        count = $visits.Count
        maxVisits = $config.MaxVisits
        remaining = $config.MaxVisits - $visits.Count
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    }

    Write-KrJsonResponse -InputObject $response -StatusCode 200
}

# Route: POST /reset - Reset visit counter
Add-KrMapRoute -Verbs Post -Pattern '/reset' -ScriptBlock {
    $visits = Get-KrSharedState -Name 'Visits'
    $visits.Count = 0

    Write-KrJsonResponse -InputObject @{
        message = 'Visit counter reset'
        count = 0
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    } -StatusCode 200
}

# Route: PUT /config - Update configuration
Add-KrMapRoute -Verbs Put -Pattern '/config' -ScriptBlock {
    $body = Get-KrRequestBody
    $config = Get-KrSharedState -Name 'Config'

    if ($body.maxVisits) {
        $config.MaxVisits = [int]$body.maxVisits
    }
    if ($body.appName) {
        $config.AppName = $body.appName
    }

    Write-KrJsonResponse -InputObject @{
        message = 'Configuration updated'
        config = $config
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    } -StatusCode 200
}

# Route: DELETE /state/{name} - Remove shared state variable
Add-KrMapRoute -Verbs Delete -Pattern '/state/{name}' -ScriptBlock {
    $name = Get-KrRequestRouteParam -Name 'name'

    if ($name -in @('Visits', 'Config', 'StartTime')) {
        Write-KrJsonResponse -InputObject @{
            error = 'Cannot remove core state variables'
            name = $name
        } -StatusCode 403
        return
    }

    $removed = Remove-KrSharedState -Name $name

    if ($removed) {
        Write-KrJsonResponse -InputObject @{
            message = 'State variable removed'
            name = $name
        } -StatusCode 200
    } else {
        Write-KrJsonResponse -InputObject @{
            error = 'State variable not found'
            name = $name
        } -StatusCode 404
    }
}

# Route: GET /state/{name} - Get specific shared state value
Add-KrMapRoute -Verbs Get -Pattern '/state/{name}' -ScriptBlock {
    $name = Get-KrRequestRouteParam -Name 'name'
    $value = Get-KrSharedState -Name $name

    if ($null -eq $value) {
        Write-KrJsonResponse -InputObject @{
            error = 'State variable not found'
            name = $name
        } -StatusCode 404
        return
    }

    Write-KrJsonResponse -InputObject @{
        name = $name
        value = $value
        type = $value.GetType().FullName
        runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.Name
    } -StatusCode 200
}

# Start the server
Start-KrServer

Step-by-step

  1. Initialize root: Initialize-KrRoot -Path $PSScriptRoot for predictable relative paths.
  2. Server & listener: New-KrServer; Add-KrEndpoint -Port 5000 -IPAddress Loopback.
  3. Define shared state using Set-KrSharedState:
    • Set-KrSharedState -Name 'Visits' -Value @{ Count = 0 } — thread-safe visit counter
    • Set-KrSharedState -Name 'Config' -Value @{ MaxVisits = 100; AppName = 'SharedStateDemo' } — configuration
    • Set-KrSharedState -Name 'StartTime' -Value (Get-Date) -AllowsValueType — server start time
  4. Apply config: Enable-KrConfiguration freezes the configuration graph.
  5. Map routes that interact with shared state:
    • GET /info — retrieve configuration and uptime
    • GET /visits — show current visit count
    • POST /visit — increment visit counter (atomic via Update-KrSynchronizedCounter)
    • POST /reset — reset visit counter to zero
    • PUT /config — update configuration values
    • DELETE /state/{name} — remove a shared state variable
    • GET /state/{name} — retrieve a specific shared state value
  6. Start: Start-KrServer.

Behavior contract

  • Inputs: REST API with JSON request/response bodies.
  • Outputs: JSON responses with status codes (200, 403, 404).
  • Shared state: Variables created with Set-KrSharedState are accessible across all request runspaces and languages.

How shared state works

The Kestrun shared state system provides a thread-safe, centralized store for application-wide variables:

Key characteristics

  • Thread-safe: All operations are synchronized to prevent race conditions.
  • Cross-runspace: State is shared across all PowerShell runspaces handling requests.
  • Cross-language: Accessible from PowerShell, C#, and VB.NET routes.
  • Server-scoped: Each server instance has its own isolated shared state.
  • Global option: Use -Global parameter for process-wide shared state across multiple servers.

Cmdlet overview

Cmdlet Purpose When to use
Set-KrSharedState Create or update shared variable Before Enable-KrConfiguration for initialization; in routes for updates
Get-KrSharedState Retrieve shared variable value In routes to access shared data
Remove-KrSharedState Delete shared variable When cleaning up or removing state

Usage patterns

Initialize state before configuration:

Set-KrSharedState -Name 'Counter' -Value @{ Count = 0 }
Enable-KrConfiguration

Read state in routes:

Add-KrMapRoute -Verbs Get -Pattern '/count' -ScriptBlock {
    $counter = Get-KrSharedState -Name 'Counter'
    Write-KrJsonResponse @{ count = $counter.Count }
}

Update state in routes:

Add-KrMapRoute -Verbs Post -Pattern '/increment' -ScriptBlock {
    $counter = Get-KrSharedState -Name 'Counter'
    # Atomic increment to avoid lost updates under concurrency
    Update-KrSynchronizedCounter -Table $counter -Key 'Count' -By 1
    Write-KrJsonResponse @{ count = $counter.Count }
}

Atomic numeric increments

For counters under high concurrency you can lose updates using $counter.Count++ because multiple runspaces can read the same value then write back overlapping results (classic read‑modify‑write race). Use the atomic helper to guarantee correctness:

Add-KrMapRoute -Verbs Post -Pattern '/increment-atomic' -ScriptBlock {
    $counter = Get-KrSharedState -Name 'Counter'
    # Performs an atomic increment on the 'Count' key
    Update-KrSynchronizedCounter -Table $counter -Key 'Count' -By 1
    Write-KrJsonResponse @{ count = $counter.Count }
}

Update-KrSynchronizedCounter internally uses locking via Monitor.Enter/Exit for thread-safe increments, ensuring each request contributes exactly one increment even when dozens of runspaces hit the route simultaneously.

When to prefer it:

  • Hot counters (frequent updates per second)
  • Metrics / rate limiting logic
  • Any place inaccurate totals would mislead downstream logic

When simple ++ is acceptable:

  • Low traffic / demo scenarios
  • Non-critical values where occasional drift is tolerable

Remove state:

Remove-KrSharedState -Name 'TempCache'

Advantages over manual variables

Aspect Manual Variables (4.1) Shared State Cmdlets (4.2)
Thread safety Must use thread-safe types Collection storage only; value mutations require explicit locking or atomic helpers
Value types Copied per runspace Can be shared with -AllowsValueType
Cross-language PowerShell only Works in C#/VB.NET too
Runtime updates Limited Full create/read/update/delete
Discoverability Variable scope rules Explicit get/set API

Try it

Follow these steps to explore the shared state management features.

1. Save & run the sample

Copy pwsh/tutorial/examples/4.2-Shared-State.ps1 into a file (e.g., shared-state.ps1) and start it:

pwsh .\shared-state.ps1

You should see startup log lines:

info: Kestrun[0] Listening on http://127.0.0.1:5000

Leave this session running.

2. Check initial state

PowerShell:

Invoke-RestMethod -Uri http://127.0.0.1:5000/info | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/visits | ConvertTo-Json

Expected output:

{
  "appName": "SharedStateDemo",
  "maxVisits": 100,
  "uptimeSeconds": 2.45,
  "runspace": "Runspace1"
}

{
  "count": 0,
  "runspace": "Runspace2"
}

3. Increment visits

1..5 | ForEach-Object {
    Invoke-RestMethod -Uri http://127.0.0.1:5000/visit -Method Post | ConvertTo-Json
}

Example output:

{
  "count": 1,
  "maxVisits": 100,
  "remaining": 99,
  "runspace": "Runspace3"
}
...

4. Concurrent updates

Test thread-safety with concurrent requests:

(1..20 | ForEach-Object {
    Start-Job { Invoke-RestMethod -Uri http://127.0.0.1:5000/visit -Method Post }
} | Receive-Job -Wait -AutoRemoveJob).count | Measure-Object -Sum

Verify the final count matches:

Invoke-RestMethod -Uri http://127.0.0.1:5000/visits

The count should be accurate with no lost increments.

5. Update configuration

$body = @{ maxVisits = 200; appName = 'UpdatedDemo' } | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/config -Method Put -Body $body -ContentType 'application/json' | ConvertTo-Json

Verify the update:

Invoke-RestMethod -Uri http://127.0.0.1:5000/info | ConvertTo-Json

6. Reset counter

Invoke-RestMethod -Uri http://127.0.0.1:5000/reset -Method Post | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/visits | ConvertTo-Json

7. Query specific state

Invoke-RestMethod -Uri http://127.0.0.1:5000/state/Config | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:5000/state/StartTime | ConvertTo-Json

8. Clean up

Stop the server (Ctrl+C). State is reset on restart.

Advanced patterns

Global shared state

Use -Global to share state across multiple Kestrun server instances in the same process:

Set-KrSharedState -Global -Name 'ProcessMetrics' -Value @{ TotalRequests = 0 }

# Accessible from any server instance:
$metrics = Get-KrSharedState -Global -Name 'ProcessMetrics'

Value types with -AllowsValueType

By default, shared state requires reference types. For value types (int, bool, etc.), use -AllowsValueType:

Set-KrSharedState -Name 'RequestCount' -Value 0 -AllowsValueType

Note: Value types are boxed and synchronized, but prefer reference types (hashtables, objects) for better ergonomics.

Custom classes

Store custom PowerShell class instances:

class AppMetrics {
    [int]$RequestCount = 0
    [datetime]$LastRequest = [datetime]::MinValue

    [void]RecordRequest() {
        $this.RequestCount++
        $this.LastRequest = Get-Date
    }
}

Set-KrSharedState -Name 'Metrics' -Value ([AppMetrics]::new())

# In routes:
$metrics = Get-KrSharedState -Name 'Metrics'
$metrics.RecordRequest()

Best practices

  • Initialize early: Call Set-KrSharedState before Enable-KrConfiguration for startup state.
  • Use reference types: Hashtables and custom objects work best; avoid plain value types unless using -AllowsValueType.
  • Use atomic counter helper: Prefer Update-KrSynchronizedCounter for numeric increments to avoid lost updates.
  • Validate inputs: Check for null and validate data before updating shared state.
  • Keep state minimal: Store only what’s needed; externalize large or persistent data.
  • Document state schema: Comment expected structure and usage patterns.
  • Consider global vs server scope: Use server-scoped (default) for isolated instances; -Global for cross-server state.

Common pitfalls

Pitfall Symptom Fix
Null reference $null when accessing state Verify name is correct and state was set
State not shared Each runspace has different value Use Set-KrSharedState, not plain variables
Lost updates Concurrent updates missing State is thread-safe; check logic for overwrites
Value type confusion Changes not reflected Use reference types or -AllowsValueType

Troubleshooting

Symptom Cause Resolution
Variable is $null Never set or wrong name Verify Set-KrSharedState was called
State not updating Overwriting reference Mutate properties, don’t reassign variable
Memory growth Unbounded keys/collections Implement eviction or size limits
Cross-server state not working Using server scope Add -Global to both set and get

Comparison with 4.1 Shared Variables

The previous tutorial (4.1 Shared Variables) demonstrated manual variable sharing by declaring variables before Enable-KrConfiguration. This tutorial introduces the recommended approach using dedicated cmdlets:

When to use manual variables (4.1):

  • Rapid prototyping or simple demos
  • PowerShell-only routes
  • Variables already thread-safe (e.g., ConcurrentDictionary)

When to use shared state cmdlets (4.2):

  • Production code requiring cross-language support
  • Runtime state management (create/update/delete)
  • Need for global state across server instances
  • Value type sharing
  • Better discoverability and maintainability

References


Previous / Next

Go back to Shared Variables or continue to Logging.

See also

For deeper architectural details (implicit vs explicit, atomic patterns, runspace lifecycle) see the Shared State & Runspaces Guide and Tutorial 4.1 for implicit variable sharing.