SessionID changing across different instances in Azure (and probably in a web farm)
Asked Answered
P

3

6

I have a problem with an Azure project with one WebRole but multiple instances that uses cookieless sessions. The application doesn't need Session storage, so it's not using any session storage provider, but I need to track the SessionID. Apparently, the SessionID should be the same accross the WebRole instances, but it changes suddently w/o explanation. We are using the SessionID to track some data, so it's very important.

In order to reproduce the issue:

  1. Create a Cloud Project.

  2. Add a ASP.NET Web Role. The code already in it will do.

  3. Open Default.aspx

  4. Add a control to see the current SessionID and a button to cause a postback

            <p><%= Session.SessionID %></p>
            <asp:Button ID="Button1" runat="server" Text="PostBack" onclick="Button1_Click" />
    
  5. Add a event handler for button that will delay the response a bit:

    protected void Button1_Click(object sender, EventArgs e)
    {
        System.Threading.Thread.Sleep(150);
    }
    
  6. Open Web.Config

  7. Enable cookieless sessions:

    <system.web>
            <sessionState cookieless="true" />
    </system.web>
    
  8. Run the project, and hit fast and repeteadly the "PostBack" button for a while giving attention to the session id in the address bar. Nothing happens, the session id is always the same :). Stop it.

  9. Open ServiceConfiguration.csfg

  10. Enable four instances:

    <Instances count="4" />
    
  11. Ensure that in the Web.config there is a line related with the machine key that has been added automatically by Visual Studio. (at the end of system.web).

  12. Rerun the project, hit fast and repeteadly the "Postback" button for a while and give attention to the session id in the address bar. You'll see how the SessionID changes after a while.

Why is this happening? As far as I know, if all machines share the machineKey, the session should be the same across them. With cookies there are no problems, the issue apparently is just when cookieless sessions are used.

My best guess, is that something wrong is happening when there are several instances, when the SessionID generated in one WebRole goes to another, is rejected and regenerated. That doesn't make sense, as all the WebRoles have the same machineKey.

In order to find out the problem, and see it more clearly, I created my own SessionIDManager:

public class MySessionIDManager : SessionIDManager
{
    public override string CreateSessionID(HttpContext context)
    {
        if (context.Items.Contains("AspCookielessSession"))
        {
            String formerSessionID = context.Items["AspCookielessSession"].ToString();

           // if (!String.IsNullOrWhiteSpace(formerSessionID) && formerSessionID != base.CreateSessionID(context))
               // Debugger.Break();

            return formerSessionID;
        }
        else
        {
            return base.CreateSessionID(context);
        }
    }
}

And to use it change this line in the WebConfig:

    <sessionState cookieless="true" sessionIDManagerType="WebRole1.MySessionIDManager" />

Now you can see that the SessionID doesn't change, no matter how fast and for how long you hit. If you uncomment those two lines, you will see how ASP.NET is creating a new sessionID even when there is already one.

In order to force ASP.NET to create a new session, just a redirect to an absolute URL in your site:

 Response.Redirect(Request.Url.AbsoluteUri.Replace(Request.Url.AbsolutePath, String.Empty));

Why is this thing happening with cookieless sessions?

How reliable is my solution in MySessionIDManager ?

Kind regards.

UPDATE:

  • I've tried this workaround: User-Specified Machine Keys Overwritten by Site-Level Auto Configuration, but the problem still stands.

    public override bool OnStart()
    {
        // For information on handling configuration changes
        // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
    
        using (var server = new ServerManager())
        {
            try
            {
                // get the site's web configuration
                var siteNameFromServiceModel = "Web"; // update this site name for your site. 
                var siteName =
                    string.Format("{0}_{1}", RoleEnvironment.CurrentRoleInstance.Id, siteNameFromServiceModel);
                var siteConfig = server.Sites[siteName].GetWebConfiguration();
    
                // get the appSettings section
                var appSettings = siteConfig.GetSection("appSettings").GetCollection()
                    .ToDictionary(e => (string)e["key"], e => (string)e["value"]);
    
                // reconfigure the machine key
                var machineKeySection = siteConfig.GetSection("system.web/machineKey");
                machineKeySection.SetAttributeValue("validationKey", appSettings["validationKey"]);
                machineKeySection.SetAttributeValue("validation", appSettings["validation"]);
                machineKeySection.SetAttributeValue("decryptionKey", appSettings["decryptionKey"]);
                machineKeySection.SetAttributeValue("decryption", appSettings["decryption"]);
    
                server.CommitChanges();
                _init = true;
            }
            catch
            {
            }
        }
        return base.OnStart();
    }
    
  • I've also tried this about put a session start handler and add some data, but no luck.

    void Session_Start(object sender, EventArgs e)
    {
        Session.Add("dummyObject", "dummy");
    }
    

Bounty up!

Prefrontal answered 26/2, 2011 at 14:13 Comment(2)
Not sure I can help with the main problem you are seeing. However, from what I've read the web.config you upload should not have a machine key in it - I believe this is instead syncrhonised within the Azure VM. Good luck with the main problem!Volteface
As far as I know, the machineKey is added automatically by Visual Studio. I'll investigate that as well, thanks!Prefrontal
D
3

In short, unless you use cookies or a session provider there is no way for the session id to pass from one web role instance to the other. The post you mention says that the SessionID does NOT stay the same across web roles if you don't use cookies or session storage.
Check this previous question for ways to handle state storage in Azure, e.g. using Table Storage

The machineKey has nothing to do with sessions or the application domain, it is the key used to encrypt,decrypt,validate authentication and viewstate data. To verify this open SessionIDManager.CreateSessionID with Reflector. You will see that the ID value is just a random 16-byte value encoded as a string.

The AspCookielessSession value is already checked by SessionIDManager in the GetSessionID method, not CreateSessionID so the check is already finished before your code gets executed. Since the default sessionstate mode is InProc it makes sence that separate web roles will not be able to validate the session key so they create a new one.

In fact, a role may migrate to a different physical machine at any time, in which case its state will be lost. This post from the SQL Azure Team describes a way to use SQL Azure to store state for exactly this reason.

EDIT I finally got TableStorageSessionStateProvider to work in cookieless mode!

While TableStorageSessionStateProvider does support cookieless mode by overriding SessionStateStoreProviderBase.CreateUnititializedItem, it fails to handle empty sessions properly in private SessionStateStoreData GetSession(HttpContext context, string id, out bool locked, out TimeSpan lockAge,out object lockId, out SessionStateActions actions,bool exclusive). The solution is to return an empty SessionStateStoreData if no data is found in the underlying blob storage.

The method is 145 lines long so I won't paste it here. Search for the following code block

if (actions == SessionStateActions.InitializeItem) 
{
     // Return an empty SessionStateStoreData                    
     result = new SessionStateStoreData(new SessionStateItemCollection(),
}

This block returns an empty session data object when a new session is created. Unfortunately the empty data object is not stored to the blob storage.

Replace the first line with the following line to make it return an empty object if the blob is empty:

if (actions == SessionStateActions.InitializeItem || stream.Length==0)

Long stroy short cookieles session state works as long as the provider supports it. You'll have to decide whether using cookieless state justifies using a sample provider though. Perhaps vtortola should check the AppFabric Caching CTP. It includes out-of-the-box ASP.NET providers, is a lot faster and it definitely has better support than the sample providers. There is even a step-by-step tutorial on how to set session state up with it.

Dormer answered 2/3, 2011 at 13:54 Comment(9)
according to the comments, vtortola has tried added a real shared Session provider (table storage) and has put data in it - but the cookieless session id still isn't being persisted.Volteface
Oops, didn't see that. The provider has to be able to handle cookieless session state properly, by overriding CreateUninitializedItem. While the TableStorageSessionStateProvider does override this method, it doesn't handle empty sessions properly in one of its GetSession overrides, resulting in exceptions. Guess they weren't kidding when they said the sample providers are unsupported! I've found the single line that needs changin (see EDIT above) but I'm starting to think that maybe this provider is not so dependable after allDormer
Cool, Panagiotis - I'd agree that those early sample providers shouldn't really be used in production code - I tried using the Membership provider and it didn't turn out nicely. Given your results, I'd probably recommend that vtortola doesn't use the table storage provider, nor the caching one - probably better just to write a new dummy provider - after all vtortola never actually wants to store any session data.Volteface
I tried this and it works. I removed all references to blob storage from the provider and modified the aforementioned GetSession method to always return a new SessionStateStoreData object. I wouldn't reject the Caching provider. Table transactions cost money and caching may prove to be cheaper than plain Table storage. On the other hand, you CAN store session keys in memory only check the table storage if you can't find the keys in your local copy, although you would still need to update the expiration values in the table.Dormer
Nice, so I tried with another buggy thing haha. I'll give it a try as soon as I can!. Thanks a million!Prefrontal
oh BTW, AppFabric Caching cannot be used in commercial applications in production, that is what I'm gonna do tomorrow :DPrefrontal
Still doesn't work in my example, could you provide an example I can download?Prefrontal
Well I supposed I'm doing something wrong, I trust in your word anyway :)Prefrontal
sorry, apparently I had to click on the bounty button instead the "answer" button and you get only 50 karma :(Prefrontal
V
1

Sounds tricky.

I have one suggestion/question for you. Don't know if it will help - but you sound like you're ready to try anything!

It sounds like maybe the session manager on the new machine is checking the central session storage provider and, when it finds that the session storage is empty, then it's issuing a new session key.

I think a solution may come from: - using Session_Start as you have above in order to insert something into Session storage - plus inserting a persistent Session storage provider of some description into the web.config - e.g. some of the oldest Azure samples provide a table based provider, or some of the newer samples provide an AppFabric caching solution.

I know your design is not using the session storage, but maybe you need to put something in (a bit like your Session_Start), plus you need to define something other than in-process session management.

Alternatively, you need to redesign your app around something other than ASP.NET sessions.

Hope that helps - good luck!

Volteface answered 28/2, 2011 at 19:25 Comment(4)
That's the problem. I have to use cookieless sessions because, for this specific project, session has to be in the URL, it cannot be cookies. As the project was already working in a normal web server and it's big and complex, I cannot afford at this point develop my own cookieless session system :D So far the workaround pictured in "MySessionIDManager" class is doing the trick, but still I want to know WHY is this happening, because maybe it's an error or bug, and it could bring more complications in the future. Anyway, I'll try what you say, it could be an interesting experiment :D. Thanks!Prefrontal
My theory is that when ASP gets the SessionID it checks that SessionID against the Session Storage - which by default is a local in-process session store on each server. If that SessionID isn't found, then I think ASP allocates a new Session with a new ID. We'll know more about whether this is true after you try the experiment :DVolteface
I've tested it with the TableStorageSessionStateProvider and still not working, this is insane :O. If I set it with cookies it works.Prefrontal
If it doesn't work with data actually in the Session and with a genuine shared Session provider in place, then that's a bug - might be worth contacting someone like Steve Marx to check.Volteface
D
1

I experienced the same problem and after much research and debugging I found that the issue occurred because the "virtual servers" in the Azure SDK map the websites to different paths in the IIS metabase. (You can see this through through Request.ServerVariables["APPL_MD_PATH"].)

I just found this out now but wanted to post this so people could get working on testing it. My theory is that this problem may go away once it's published out to Azure proper. I'll update with any results I find.

Dove answered 10/3, 2011 at 18:10 Comment(2)
I uploaded the site to a staging environment on Azure and the problem hasn't reoccurred.Dove
That's interesting, I will check it out this weekend as well :)Prefrontal

© 2022 - 2024 — McMap. All rights reserved.