Embedio: Session management for v3.0

Created on 10 May 2019  ·  8Comments  ·  Source: unosquare/embedio

Let's talk about an usage scenario:

• request #1 asks for session data, so I give it a sessionInfo object obtained by loading data from storage
• request #2 asks for the same session _before_ request #1 is over, so what? Do i give it the same object, or do I load data again?
• request #1 finishes and I save session data: should I lock session data while I save it? In this case, request #2 will be slowed down if it tries to set data in the meanwhile. But if I don't do it, I risk exceptions while I enumerate the dictionary.
• request #2 finishes and I save session data. Again. As in, like, twice in a few milliseconds.

I'd like it to be as easy as possible to develop extensions for EmbedIO. That's why I'm trying to keep externally-implementable interfaces as simple to implement as possible.

enhancement

All 8 comments

To clarify this for everyone: this issue was born as a conversation between @geoperez and myself, while working on version 3.0 of EmbedIO. What we're trying to achieve is this:

  • The session object will be directly available as a property of IHttpContext

  • The lifecycle of session objects will be entirely managed by the web server

  • Session management requires two distinct helper objects:

    • a session ID manager, in charge of retrieving / storing / deleting session IDs in HTTP contexts (possible implementations may use cookies, query strings, or... I don't know what else, but let's keep options open)

    • a session storage, in charge of retrieving / storing / deleting session data in a storage medium (memory, files, a database...)

The workflow should be more or less as follows:

  • the first time a session is needed during processing of a request:

    • pass the HTTP context to the ID manager to retrieve the session ID

    • if there's no session ID in the context, create a new unique ID and call the session ID manager again to store it in the HTTP context

    • call the session storage to retrieve data for the session (if any)

    • construct the session object and use it

  • when the processing of a request is finished:

    • if session data has been cleared, call the ID manager to delete the session ID and the session storage to delete session data

    • otherwise, call the session storage to save the session data.

Important goals here are:

  • implementing a session ID manager or a session storage should be as simple as possible

  • internal data structures should be completely abstracted away from helper objects


Here are the two interfaces for helper objects so far (they're not even committed yet):
```C#
// ISessionIdManager.cs
namespace EmbedIO
{
///


/// Represents the session ID manager for a web server.
///

public interface ISessionIdManager
{
///
/// Retrieves the session identifier for a HTTP context.
/// If no session exists for the context,
/// this method must return .

///

/// The .
/// A identifying the session, or .
///
///
///
string RetrieveSessionId(IHttpContext context);

    /// <summary>
    /// <para>Stores a session identifier in a HTTP context.</para>
    /// </summary>
    /// <param name="context">The <see cref="IHttpContext"/>.</param>
    /// <param name="id">The session identifier.</param>
    /// <seealso cref="SessionInfo.Id"/>
    /// <seealso cref="SessionInfo.IdComparison"/>
    /// <seealso cref="SessionInfo.IdComparer"/>
    void StoreSessionId(IHttpContext context, string id);

    /// <summary>
    /// <para>Deletes the session identifier (if any) from a HTTP context.</para>
    /// </summary>
    /// <param name="context">The <see cref="IHttpContext"/>.</param>
    /// <seealso cref="SessionInfo.Id"/>
    /// <seealso cref="SessionInfo.IdComparison"/>
    /// <seealso cref="SessionInfo.IdComparer"/>
    void DeleteSessionId(IHttpContext context);
}

}

```C#
// ISessionStorage.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace EmbedIO
{
    /// <summary>
    /// Represents the session storage for a web server.
    /// </summary>
    public interface ISessionStorage
    {
        /// <summary>
        /// Retrieves data for the session identified by the given <paramref name="id"/>.
        /// </summary>
        /// <param name="id">The ID of the session data to retrieve.</param>
        /// <param name="initializer">
        /// <para>A callback that initializes the session object.</para>
        /// <para>The callback's only parameter is the UTC date / time of session expiration.</para>
        /// <para>It will return a second callback, which shall be called
        /// for each key / value pair of retrieved session data.</para>
        /// </param>
        /// <returns>A <see cref="Task"/> object that represents the ongoing operation.</returns>
        /// <remarks>
        /// <para>If no session with the given <paramref name="id"/> is found in storage,
        /// this method must simply return without calling
        /// the <paramref name="initializer"/> callback.</para>
        /// <para>This method must treat an expired session as if it was not found,
        /// and delete it from storage automatically.</para>
        /// <para>Automatic purging of expired sessions is left to implementations.</para>
        /// </remarks>
        Task RetrieveSessionAsync(string id, Func<DateTime, Action<string, object>> initializer);

        /// <summary>
        /// Stores data for the session identified by the given <paramref name="id"/>.
        /// </summary>
        /// <param name="id">The ID of the session data to save.</param>
        /// <param name="expiresAt">The UTC date and time of session expiration.</param>
        /// <param name="data">An enumeration of the data to store.</param>
        /// <returns>A <see cref="Task"/> object that represents the ongoing operation.</returns>
        Task StoreSessionAsync(string id, DateTime expiresAt, IEnumerable<KeyValuePair<string, object>> data);

        /// <summary>
        /// Delete data for the session identified by the given <paramref name="id"/>.
        /// </summary>
        /// <param name="id">The ID of the session data to delete.</param>
        /// <returns>A <see cref="Task"/> object that represents the ongoing operation.</returns>
        Task DeleteSessionAsync(string id);
    }
}

Given the background, the question is: how shall we deal with concurrency?

OK, now read the first post again and things should be clearer. 😉

Any help or feedback is highly appreciated! Thanks in advance.

@mariodivece, you wrote the initial session state management for EmbedIO. Do you have any thoughts?

EmbedIO v2 has no concept of session data storage, nor is there any way to implement it over the existing IWebServer, IWebModule, and ISessionWebModule interfaces.

A minimum viable solution for v2 would be as follows:

  • add a SaveSession method to ISessioWebModule; this would be a no-op in LocalSessionModule, but may be implemented by other session modules to use e.g. SQLite;

  • WebServer should call SaveSession once it has finished handling the request.

That's admittedly a tad too _minimum_. We should also have a mechanism to flag a session as "dirty" when session data changes (a façade on the data dictionary could do the trick) and save only newly-created and modified sessions.

At this point LocalSessionModule would need to store _copies_ of session data, instead of the actual session objects. Otherwise, if two or more requests use the same session at the same time, one of them changing data would trigger saving when each one of them is finished handling. But this can be done, too.

Let's say we implement all of the above in EmbedIO v2. Now we're left with exactly the same question that's in the first post! 😱


The fact is, session management is harder to get right than it seemed at first. I wonder whether it's even worth the effort for a tiny embedded web server, for which LocalSessionModule has proven good enough so far.

Given the above, I'd go for a lighter approach, as follows:

  • add the necessary hooks into WebServerBase request handling to make it possible to develop a session manager over existing v3.0 APIs;

  • modify LocalSessionModule to use IHttpContext.Items to store the request's session object, with a unique key defined as public static readonly object SessionKey = new object();;

  • at this point, ISessionWebModule and session-related stuff in IHttpContext and WebServerBase could be removed, thus leaving a clean slate for possible future session implementations.

Any thoughts?

I like the idea to integrate LocalSessionModule with IHttpContext.Items. Are you still working on this right?

Of course I'm working on it. 😁 I've made advancements in other areas, too, but the modifications were kinda intertwined, so I couldn't just commit something in a "clean" manner. You can expect a commit by tomorrow with LocalSessionModule revived, plus more.

You can expect a commit by tomorrow [...]

Yeah, just as I said. _Tomorrow._ :grin:

Sorry guys, these are busy days. I'm almost through with session management though, and I promise it's going to rock!

Don't worry xD

New session management added in d5fc7fc405d7394ffff342163a8e41a266c9a965 and slightly improved in 03ee94386f121c6a4ab7347b0b944c373dbfc37f.

In the end it's not what was discussed here. I tried to balance simplicity and robustness. Any feedback is highly appreciated.

Was this page helpful?
0 / 5 - 0 ratings