Skip to content

Commit

Permalink
Support serializing session changesets
Browse files Browse the repository at this point in the history
Previously, the serialization of the session state to go between the remote server and the client would
deserialize/serialize every key even if it hadn't changed. As part of this change:

- The session state tracks which items are new or have been accessed (and thus potentially changed)
- Only sends a diff list rather than all the items
- Only deserializes items if they are being accessed
- Make the ISessionSerializer internal as its not the preferred way to serialize session state (use
  • Loading branch information
twsouthwick committed Dec 17, 2024
1 parent 91322f1 commit f69ed01
Show file tree
Hide file tree
Showing 13 changed files with 954 additions and 157 deletions.
85 changes: 85 additions & 0 deletions designs/session-serialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Session serialization

Session serialization is provided through the `ISessionSerializer` type. There are two modes that are available:

## Common structure

```mermaid
packet-beta
0: "M"
1-10: "Session Id (Variable length)"
11: "N"
12: "A"
13: "R"
14: "T"
15: "C"
16-24: "Key 1 Blob"
25-33: "Key 2 Blob"
34-42: "..."
43-50: "Flags (variable)"
```

Where:
- *M*: Mode
- *N*: New session
- *A*: Abandoned
- *R*: Readonly
- *T*: Timeout
- *C*: Key count

## Flags

Flags allow for additional information to be sent either direction that may not be known initially. This field was added v2 but is backwards compatible with the v1 deserializer and will operate as a no-op as it just reads the things it knows about and doesn't look for the end of a payload.

Structure:

```mermaid
packet-beta
0: "C"
1: "F1"
2: "F1L"
3-10: "Flag1 specific payload"
11: "F2"
12: "F2L"
13-20: "Flag2 specific payload"
21-25: "..."
```

Where:
- *Fn*: Flag `n`

Where `C` is the count of flags, and each `Fn` is a flag identifier an int with 7bit encoding. Each f

An example is the flag section used to indicate that there is support for diffing a session state on the server:

```mermaid
packet-beta
0: "1"
1: "100"
2: "0"
```

## Full Copy (Mode = 1)

The following is the structure of the key blobs when the full state is serialized:

```mermaid
packet-beta
0-10: "Key name"
11-20: "Serialized value"
```

## Diffing Support (Mode = 2)

The following is the structure of the key blobs when only the difference is serialized:

```mermaid
packet-beta
0-10: "Key name"
11: "S"
12-20: "Serialized value"
```

Where:
- *S*: The session state. Supported values include those defined by `SessionItemChangeState`

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,21 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

public interface ISessionSerializer
{
/// <summary>
/// Deserializes a session state.
/// </summary>
/// <param name="stream">The serialized session stream.</param>
/// <param name="token">A cancellation token</param>
/// <returns>If the stream defines a serialized session changeset, it will also implement <see cref="ISessionStateChangeset"/>.</returns>
Task<ISessionState?> DeserializeAsync(Stream stream, CancellationToken token);

/// <summary>
/// Serializes the session state. If the <paramref name="state"/> implements <see cref="ISessionStateChangeset"/> it will serialize it
/// in a mode that only tracks the changes that have occurred.
/// </summary>
/// <param name="state"></param>
/// <param name="stream"></param>
/// <param name="token"></param>
/// <returns></returns>
Task SerializeAsync(ISessionState state, Stream stream, CancellationToken token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

public interface ISessionStateChangeset : ISessionState
{
IEnumerable<SessionStateChangeItem> Changes { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

public enum SessionItemChangeState
{
Unknown = 0,
NoChange = 1,
Removed = 2,
Changed = 3,
New = 4,
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

[DebuggerDisplay("{State}: {Key,nq}")]
public readonly struct SessionStateChangeItem(SessionItemChangeState state, string key) : IEquatable<SessionStateChangeItem>
{
public SessionItemChangeState State => state;

public string Key => key;

public override bool Equals(object? obj) => obj is SessionStateChangeItem item && Equals(item);

public override int GetHashCode()
=> State.GetHashCode() ^ Key.GetHashCode();

public bool Equals(SessionStateChangeItem other) =>
State == other.State
&& string.Equals(Key, other.Key, StringComparison.Ordinal);

public static bool operator ==(SessionStateChangeItem left, SessionStateChangeItem right)
{
return left.Equals(right);
}

public static bool operator !=(SessionStateChangeItem left, SessionStateChangeItem right)
{
return !(left == right);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Web;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;

Expand All @@ -22,11 +23,43 @@ public static void CopyTo(this ISessionState result, HttpSessionStateBase state)
}

state.Timeout = result.Timeout;

if (result is ISessionStateChangeset changes)
{
UpdateFromChanges(changes, state);
}
else
{
Replace(result, state);
}
}

private static void UpdateFromChanges(ISessionStateChangeset from, HttpSessionStateBase state)
{
foreach (var change in from.Changes)
{
if (change.State is SessionItemChangeState.Changed or SessionItemChangeState.New)
{
state[change.Key] = from[change.Key];
}
else if (change.State is SessionItemChangeState.Removed)
{
state.Remove(change.Key);
}
else if (change.State is SessionItemChangeState.Unknown)
{

}
}
}

private static void Replace(ISessionState from, HttpSessionStateBase state)
{
state.Clear();

foreach (var key in result.Keys)
foreach (var key in from.Keys)
{
state[key] = result[key];
state[key] = from[key];
}
}
}
118 changes: 118 additions & 0 deletions src/Services/SessionState/BinarySessionSerializer.ChangesetWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

[System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "Source shared with .NET Framework that does not have the method")]
internal partial class BinarySessionSerializer : ISessionSerializer
{
private readonly struct ChangesetWriter(ISessionKeySerializer serializer)
{
public List<string>? Write(ISessionStateChangeset state, BinaryWriter writer)
{
writer.Write(ModeDelta);
writer.Write(state.SessionID);

writer.Write(state.IsNewSession);
writer.Write(state.IsAbandoned);
writer.Write(state.IsReadOnly);

writer.Write7BitEncodedInt(state.Timeout);
writer.Write7BitEncodedInt(state.Count);

List<string>? unknownKeys = null;

foreach (var item in state.Changes)
{
writer.Write(item.Key);

// New with V2 serializer
if (item.State is SessionItemChangeState.NoChange or SessionItemChangeState.Removed)
{
writer.Write7BitEncodedInt((int)item.State);
}
else if (serializer.TrySerialize(item.Key, state[item.Key], out var result))
{
writer.Write7BitEncodedInt((int)item.State);
writer.Write7BitEncodedInt(result.Length);
writer.Write(result);
}
else
{
(unknownKeys ??= []).Add(item.Key);
writer.Write7BitEncodedInt((int)SessionItemChangeState.Unknown);
}
}

writer.WriteFlags([]);

return unknownKeys;
}

public SessionStateCollection Read(BinaryReader reader)
{
var state = SessionStateCollection.CreateTracking(serializer);

state.SessionID = reader.ReadString();
state.IsNewSession = reader.ReadBoolean();
state.IsAbandoned = reader.ReadBoolean();
state.IsReadOnly = reader.ReadBoolean();
state.Timeout = reader.Read7BitEncodedInt();

var count = reader.Read7BitEncodedInt();

for (var index = count; index > 0; index--)
{
var key = reader.ReadString();
var changeState = (SessionItemChangeState)reader.Read7BitEncodedInt();

if (changeState is SessionItemChangeState.NoChange)
{
state.MarkUnchanged(key);
}
else if (changeState is SessionItemChangeState.Removed)
{
state.MarkRemoved(key);
}
else if (changeState is SessionItemChangeState.Unknown)
{
state.AddUnknownKey(key);
}
else if (changeState is SessionItemChangeState.New or SessionItemChangeState.Changed)
{
var length = reader.Read7BitEncodedInt();
var bytes = reader.ReadBytes(length);

if (serializer.TryDeserialize(key, bytes, out var result))
{
if (result is not null)
{
state[key] = result;
}
}
else
{
state.AddUnknownKey(key);
}
}
}

foreach (var (flag, payload) in reader.ReadFlags())
{
// No flags are currently read
}

return state;
}
}
}
Loading

0 comments on commit f69ed01

Please sign in to comment.