4 – Partitioning Multi-Tenant Applications
This chapter examines architectural and implementation considerations in the Surveys application from the perspective of building a multi-tenant application. Questions such as how to partition the application, how users will interact with the application, how you can configure it for multiple tenants, and how you handle session state and caching are all directly relevant to a multi-tenant architecture. This chapter describes how Tailspin resolved these questions for the Surveys application. For other applications, different choices may be appropriate.
Partitioning a Windows Azure Application
There are two related reasons for partitioning a Windows Azure application. First, physically partitioning the application enables you to scale the application out. For example, by running multiple instances of the worker roles in the application you can achieve higher throughput and faster processing times. Second, in a multi-tenant application, you either logically or physically partition the application to provide isolation between the tenants. Isolating tenants’ data helps to ensure that data belonging to a tenant is kept private but also helps to manage the application. For example, you may want to provide different levels of service to different tenants by scaling them out differently, or provide a different set of features to some tenants based on the type of subscription they have.
A Windows Azure application is typically comprised of multiple elements such as worker roles, web roles, queues, storage, and caching. If your application is a multi-tenant application, you can chose between different models for each of these elements:
- Single instance, multi-tenant model. For example, a single instance of a queue handles messages for all the tenants in your application.
- Multi-instance, single-tenant model. For example, each tenant has its own private message queue.
- Multi-instance, multi-tenant model. For example, premium tenants each have their own queues, but standard tenants use a shared queue.
Chapter 2, “Hosting a Multi-Tenant Application on Windows Azure,” describes the differences between these models in more detail, along with a discussion of criteria you should consider when you are choosing between them for any particular part of the application. Chapter 3, “Choosing a Multi-Tenant Data Architecture,” addresses these issues in relation to data storage in a Windows Azure application. This chapter focuses on partitioning web and worker roles, queues, and caches. It examines this partitioning from the perspective of the choices that Tailspin made in the design and implementation of the Surveys application.
Figure 1 illustrates the relationships between the various Windows Azure elements discussed in this chapter.
Figure 1
Key Windows Azure elements discussed in this chapter
Some key points to note from Figure 1 are:
- Administrative access is at the level of a Windows Azure subscription using a Windows Account or a Management API key.
- A Windows Azure subscription can contain multiple cloud services and multiple storage accounts.
- Every cloud service is assigned a unique DNS name.
- Each cloud service and each storage account is hosted in a data center selected by the subscription administrator.
Partitioning Web and Worker Roles
A Windows Azure application is typically comprised of multiple role types. For example, the Tailspin Surveys application has two web roles (one for the public website and one for the private tenant website), and a single worker role. When you deploy a web or worker role to Windows Azure, you deploy it to a cloud service that itself is part of a Windows Azure subscription. The options available for partitioning a deployment by tenant are described in the following table:
Partitioning scheme |
Notes |
---|---|
One subscription per tenant |
Makes it easy to bill individual tenants for the compute resources they consume. Enables tenants to provide their own compute resources and then manage them. During the provisioning process, the tenant would need to provide access details such as Management API keys or Microsoft account credentials if you are going to deploy the application to the tenant’s subscription. You need to be careful about the location of the cloud service that hosts the roles in relation to the location of the cloud storage the application uses in order to control data transfer costs and minimize latency. Provisioning a new Windows Azure subscription is a manual process. This does not allow tenants to share resources and costs. Each tenant must pay for all the role instances it uses. This scheme also implies one tenant per cloud service. |
Group multiple tenants in a subscription |
If your tenants can subscribe to different levels of functionality (such as basic, standard, and premium) for the application, then using different subscriptions makes it easier to track the costs of providing each level of functionality. You must still partition the application for different tenants within a subscription using one of the other partitioning schemes. |
One tenant per cloud service |
Cloud services can be provisioned automatically. Each tenant can run the application in a geographic region of their choice. Makes it easy to assign costs to tenants because each cloud service is a separate line item on the Windows Azure bill. Provides for a very high degree of tenant isolation. This does not allow tenants to share resources and costs. Each tenant must pay for all the role instances it uses. |
Group multiple tenants in a cloud service |
You can use cloud services to group tenants based on geographic region or on different levels of functionality. You must still partition the application for tenants within a cloud service using one of the other partitioning schemes, such as using multi-tenant roles. Tenants in a cloud service share the resources and costs associated with that service. |
Group multiple tenants in a role |
Requires the web or worker role to support multi-tenancy. Tenants share the resources and costs associated with the role. |
At the time of writing, there is a soft limit of 20 cores per Windows Azure subscription. With this limit, you could deploy 20 small role instances, 10 medium role instances, or 5 large role instances. This limit makes any solution that assigns tenants to roles in a one-to-one relationship within a subscription impractical for any multi-tenant application with more than a small number of tenants.
Bharath Says: | |
---|---|
|
Identifying the Tenant in a Web Role
Every cloud service must have a unique DNS name. Therefore, if you have one tenant per cloud service, each tenant can use a unique DNS name to access its copy of the application.
However, if you have multiple tenants sharing web roles within a cloud service, you must have some way to identify the tenant for each web request that accesses tenant specific data. Once you know which tenant the web request is associated with you can ensure that any queries or data updates operate only on the data or other resources that belong to that tenant.
It is your responsibility to ensure that your web roles can identify the tenant in the request, and to ensure that web roles preserve the isolation between your tenants.
There are a number of options for identifying the tenant from a web request.
- Authentication. If users accessing the site must authenticate, the site can determine the tenant from the authenticated identity.
- The URL path. For example, Tailspin’s tenants Adatum and Fabrikam could use http://tailspinsurveys.cloudapp.net/adatum/surveys and http://tailspinsurveys.cloudapp.net/fabrikam/surveys.
- The subdomain. For example, Tailspin’s tenants Adatum and Fabrikam could use http://adatum.tailspinsurveys.com and http://fabrikam.tailspinsurveys.com.
- A custom domain. For example, Tailspin’s tenants Adatum and Fabrikam could use http://surveys.adatum.com and http://surveys.fabrikam.com.
Option 1 — Using Authentication
For the first option, using authentication, you can use any standard authentication mechanism as long as your application can determine from the authenticated identity the tenant that should be associated with the request. You don’t need to include the tenant ID anywhere in the URL, so you can use the same domain and path for all tenant requests for a specific service. For example, Tailspin could use http://tailspinsurveys.cloudapp.net/surveys to enable a tenant to access a list of all its surveys.
Tailspin could also add a CNAME entry to its DNS configuration to map a custom subdomain to the surveys application. For example, Tailspin could map http://surveys.tailspin.com to http://tailspinsurveys.cloudapp.net/surveys.
This approach also enables you to protect your site using SSL by uploading to your cloud service a standard SSL certificate that identifies a single domain, and then configuring an HTTPS endpoint that uses this certificate.
Option 2 — Using the URL Path
If you are using ASP.NET MVC it is very easy to use MVC routing to identify the tenant from an element in the path. This approach is useful if you don’t want to use authentication (for example, on a public site) but you do need to identify the tenant.
You can use the same domain for all requests. For example, Tailspin could use *http://tailspinsurveys.cloudapp.net/{tenant}/surveys*,**where {tenant} is the ID of a tenant, to enable public access to a list of tenant surveys.
This approach also enables you to protect your site using SSL by uploading to your cloud service a standard SSL certificate that identifies a single domain, and then configuring an HTTPS endpoint that uses this certificate.
Option 3 — Using a Subdomain for Each Tenant
Although routing based on a subdomain is not part of the standard MVC routing functionality, it is possible to implement this behavior in MVC. For an example, see the blog post “ASP.NET MVC Domain Routing.” This approach is also useful if you don’t want to use authentication (for example, on a public site) but you do need to identify the tenant.
To be able to use a separate subdomain for each tenant you must create a CNAME entry for each tenant in your DNS configuration. For example, Tailspin’s DNS configuration might look like this:
adatum.tailspinsurveys.com CNAME tailspinsurveys.cloudapp.net
fabrikam.tailspinsurveys.com CNAME tailspinsurveys.cloupapp.net
Some DNS providers enable you to use a wildcard CNAME entry so that you don’t need to create an entry for every tenant. For example:
*.tailspinsurveys.com CNAME tailspinsurveys.cloudapp.net
Option 4 — Enabling Tenants to Use Custom Domains
There are several possible approaches to enable each tenant to map a custom domain to your Windows Azure application.
You can use host headers to map each tenant to a separate website in the cloud service. Although this enables you to have a separate website for every tenant, you must redeploy the application whenever you need to configure a new tenant because this approach requires entries in the Windows Azure service definition file.
Note
For more information about how to configure host headers in a Windows Azure application, see “How to Configure a Web Role for Multiple Web Sites,” and the useful walkthrough “Exercise 1 Registering Sites, Applications, and Virtual Directories” in the Windows Azure Training Course.
As an alternative you could allow each tenant to create its own DNS entry that maps a domain or subdomain owned by the tenant to one of your application’s DNS names. For example, Adatum could create the following DNS entry in its DNS configuration:
surveys.adatum.com CNAME adatum.tailspinsurveys.com
Or Adatum could create the following entry:
surveys.adatum.com CNAME www.tailspinsurveys.net
In either case you could use the custom domain name in your ASP.NET routing by using the technique suggested in option 3 above, or use the Request.Url class in your code to identify the domain.
Poe Says: | |
---|---|
|
Using SSL with Windows Azure Cloud Services
Windows Azure uses the name of your cloud service to generate a unique subdomain at cloudapp.net for every cloud service; for example, tailspinsurveys.cloudapp.net. You can configure your DNS provider to point one or more of your subdomains to your cloudapp.net subdomain. For example, Tailspin could configure the following CNAME entries:
adatum.tailspinsurveys.com CNAME tailspinsurveys.cloudapp.net
fabrikam.tailspinsurveys.com CNAME tailspinsurveys.cloupapp.net
admin.tailspinsurveys.com CNAME tailspinsurveys.cloupapp.net
Each Windows Azure cloud service can only have a single SSL certificate. Typically, an SSL certificate is valid for a single subdomain, so Tailspin might choose to upload an SSL certificate that is valid for admin.tailspinsurveys.com. The other two subdomains would only be able to use the HTTP protocol. However, it is possible to purchase a wildcard SSL certificate. If Tailspin purchased an SSL certificate that is valid for *.tailspinsurveys.com, then all tailspinsurveys subdomains could use the HTTPS protocol.
Poe Says: | |
---|---|
|
Identifying the Tenant in a Worker Role
In a multi-tenant Windows Azure application, tenants typically share worker role instances. Either all tenants share a worker role; or groups of tenants share each worker role. Figure 2 shows three possible models.
Figure 2
Different models for multi-tenant worker roles
In Model 1 all the tenants share all the worker role instances. In Model 2 all standard tenants use one set of worker role instances, and all premium tenants use another set; this enables you to scale the worker roles independently or provide different features for the different groups of tenants. Model 3 enables you to deploy the same worker role into different geographic regions; Tailspin plans to use this model.
Bharath Says: | |
---|---|
|
Whichever model you choose, worker roles typically receive messages from queues, and these messages cause the worker role to perform some work on behalf of a tenant. In a multi-tenant application, you must be able to identify the tenant associated with the message that the worker role receives. There are two ways to identify the tenant: either every message contains the tenant ID, or every tenant has its own queue.
In some scenarios, you may also need to identify the type of tenant, such as whether the tenant has a standard or a premium subscription. If you are processing work for both types of tenant in the same role instances then, again, you can either include the tenant type in every message or use different message queues for each type of tenant. If you want to give priority to messages from tenants with premium subscriptions, using different queues for the different tenant types makes this easy to accomplish.
Partitioning Queues
You create Windows Azure queues within a Windows Azure storage account. By default you are limited to five storage accounts per subscription but you can request additional accounts by contacting Windows Azure customer support. Although you could use different storage accounts for different tenants, this approach is typically useful only if each tenant uses its own Windows Azure subscription for its Windows Azure queues.
Jana Says: | |
---|---|
|
The previous discussion of worker role partitioning highlighted the use of queues as the way that web roles typically pass information to worker roles. You can use queues to partition the messages for tenants in three ways, as shown in Figure 3.
Figure 3
Different models for partitioning queues
The first model is useful if you do not need to distinguish between types of tenant. The second model makes it easy for the worker role to identify and process messages from different groups of tenants; Tailspin uses this approach to enable the worker role to prioritize messages from tenants with premium subscriptions. The third model is useful if you have very high volumes of messages or need the ability to manage each tenant’s queue individually.
Jana Says: | |
---|---|
|
Within a storage account you can create as many queues as you need. You are charged based on the number of messages that you send, so there is no financial penalty in using multiple queues. However there are some limits on the total number of transactions per second and the bandwidth for each storage account. For more information, see Best Practices for the Design of Large-Scale Services on Windows Azure Cloud Services on MSDN.
It is your responsibility to ensure that the application uses the correct queue for any message associated with a specific client. Windows Azure does not provide any configuration options that enable you to set permissions that limit access to a queue to a specific tenant or message type.
Note
You can also use Windows Azure Service Bus queues to transport messages from web roles to worker roles. For more information about the differences between the two types of queues, see “Windows Azure Queues and Windows Azure Service Bus Queues - Compared and Contrasted.”
Partitioning Caches
At the time of writing, Windows Azure offers two caching models: Windows Azure Caching and Shared Caching. Windows Azure Caching uses one or more of the roles in your application to host cached data, whereas Shared Caching is a separate service that hosts cached data outside of your application.
When you configure Windows Azure Caching it creates a cache that is private to your application. However all of the tenants who use the application will, by default, have access to all of the data in the cache, so it is your responsibility to ensure that the design of your application prevents tenants from accessing cached data that belongs to other tenants.
It is possible to create multiple named caches in your application either by adding dedicated caching roles or by configuring caching on multiple roles. When you read or write to a Windows Azure Caching cache, you can specify the named cache you want to use.
Jana Says: | |
---|---|
|
You can also subdivide a named Windows Azure Caching cache into named regions. Regions are useful for grouping cached items together and enable you to tag items within a region. You can then query for items within a region by using tag values. You can also remove all the items cached in a single region with a single API call.
Windows Azure Shared Caching enables you to create separate named caches, each of which has a maximum size. However, because you are charged for each shared cache that you create, this would be an expensive solution if you wanted to have one cache per tenant.
Poe Says: | |
---|---|
|
Goals and Requirements
This section describes the goals and requirements Tailspin has for the Surveys application that relate to partitioning it as a multi-tenant application.
Isolation
When subscribers to the Tailspin Surveys application access their subscription details, survey definitions, and survey results, they should only see their own data. Subscribers must authenticate with the application to gain access to this private data.
It is a key requirement of the application to protect survey designs and results from unauthorized access, and the application will use a claims-based infrastructure to achieve this. Chapter 6, “Securing Multi-Tenant Applications,” discusses the authentication and authorization mechanisms in the Tailspin Surveys application.
When survey respondents visit the Tailspin Surveys public site to complete a survey, they don’t need to authenticate. However, survey respondents should not have access to survey response data, and the response data must be private to the subscriber who published the survey.
Scalability
The Tailspin Surveys application must be scalable. The partitioning schemes that Tailspin uses for the web and worker roles and for the Windows Azure queues should not limit Tailspin’s ability to scale out the application. The web roles, the worker role, and the Windows Azure queues must be capable of being scaled independently of each other.
Three distinct groups of users will access the Surveys application: administrators at Tailspin who will manage the application, subscribers who will be creating their own custom surveys and analyzing the results, and users who will be filling out their survey responses. The first two groups will account for a very small proportion of the total number of users at any given time; the vast majority of users will be people who are filling out surveys. A large survey could have hundreds of thousands of users filling it out, while a subscriber might create a new survey only every couple of weeks.
There are three distinct groups of users who will use the Surveys application.
Furthermore, the numbers of users filling out surveys will be subject to sudden, large, short-lived spikes as subscribers launch new surveys. In addition to the different scalability requirements that arise from the two usage profiles, other requirements such as security will also vary.
Note
Chapter 5, “Maximizing Availability, Scalability, and Elasticity,” covers issues around scaling the Tailspin Surveys application in greater depth.
Accessing the Surveys Application
There will be a single URL for the subscriber website where subscribers will need to authenticate before accessing their query designs and survey results data. Additionally, all access to the application by subscribers and administrators will use HTTPS to protect the data transferred between the application and the client.
The public website where users complete surveys will not require authentication. Survey respondents should be given some indication of the identity of the survey author through the URL they use to access the survey questions, and optionally through branding applied to the web pages. In the future, Tailspin also plans to enable subscribers to include a public landing page accessible from a URL that includes the subscriber’s name, and which lists all of the surveys published by that subscriber.
Public surveys do not require HTTPS. This enables the use of DNS CNAME entries that define custom URLs for users to access and fill out these surveys.
Subscribers and survey respondents may be in different geographical locations. For example, a subscriber may be based in the U.S. but wants to perform some market research in Europe. Tailspin can minimize the latency for survey respondents by enabling subscribers to host their surveys in a datacenter located in an appropriate geographical region. However, subscribers may need to analyze the results collected from these surveys in their own geographical location.
Jana Says: | |
---|---|
|
Premium Subscriptions
Tailspin plans to offer multiple levels of subscription, initially standard and premium, with the ability to add further levels in the future. Tailspin wants to be able to offer different functionality and different levels of service with the different subscriptions. Initially, Tailspin will give priority to premium tenants ensuring that the worker role processes and saves their data faster than for standard tenants.
Designing Surveys
When a user designs a new survey in the Surveys application, they create the survey and then add questions one-by-one to the survey until it’s complete. Figure 4 shows the sequence of screens, from an early mockup of this part of the UI, when a user creates a survey with two questions.
Figure 4
Creating a survey with two questions
As you can see in the diagram, this scenario involves two different screens that require the application to maintain state as the user adds questions to the survey.
The Surveys application must maintain session state while a user designs a survey.
Overview of the Solution
This section describes the approach taken by Tailspin to meet the goals and requirements that relate to partitioning the application.
Partitioning Queues and Worker Roles
In order to enable giving priority to premium subscribers in the application Tailspin considered two alternatives for partitioning the workload for the standard and premium subscribers in the Tailspin Surveys worker role.
The first option is to use two different worker roles, one for tenants with standard subscriptions and one for tenants with premium subscriptions. Tailspin could then use two separate queues to deliver messages to the two worker roles. The second option is to use a single worker role with two queues, one queue for messages from premium subscribers and one queue for messages from standard subscribers. The worker role could then prioritize messages from the premium subscriber’s queue.
Tailspin preferred the second option because it avoids the need to manage and run different types of worker role. It can modify the priorities of the different subscriptions by adjusting configuration values for a single worker role.
In addition to the worker role using two (or more) different queues to enable it to partition the work for different groups of subscribers, the web role must choose the correct queue to use when it sends a message to the worker role.
However, Tailspin realized that there are limitations in the throughput of Windows Azure storage queues that could affect the number of concurrent messages that a queue can handle. The recommended solution is to use multiple queues and implement a round-robin process at each end of the queues to distribute the messages evenly between the queues, and to read messages from all of the queues. See the section “Azure Queues Throughput” in Chapter 7, “Managing and Monitoring Multi-tenant Applications,” for more information.
Tenant Isolation in Web Roles
Chapter 3, “Choosing a Multi-Tenant Data Architecture,” describes how the Surveys application data model partitions the data by subscriber. This section describes how the Surveys application uses MVC routing tables and areas to make sure that a subscriber sees only his or her own data.
Tailspin considered using host headers and virtual sites to enable tenants to use their own DNS name to provide access to their public surveys. However, because many smaller tenants will not want the additional complexity of managing DNS entries, and because you cannot configure a new site and host header without redeploying the application, it decided against using this option.
The developers at Tailspin decided to use the path in the application’s URL to indicate which subscriber is accessing the application. For the public Surveys website, the application doesn’t require authentication.
The URL path identifies the functional area in the application, the subscriber, and the action.
The following are three sample paths on the Subscriber website:
- /survey/adatum/newsurvey
- /survey/adatum/newquestion
- /survey/adatum
The following are two example paths on the public Surveys website:
- /survey/adatum/launch-event-feedback
- /survey/adatum/launch-event-feedback/thankyou
The application uses the first element in the path to indicate the different areas of functionality within the application. In the initial release of the Tailspin Surveys service the only functional area is survey, but in the future Tailspin expects there to be additional functional areas such as onboarding and security. The second element indicates the subscriber name, in these examples “Adatum,” and the last element indicates the action to perform, such as creating a new survey or adding a question to a survey.
You should take care when you design the path structure for your application that there is no possibility of name clashes that result from a value entered by a subscriber. In the Surveys application, if a subscriber creates a survey named “newsurvey” the path to this survey is the same as the path to the page subscribers use to create new surveys. However, the application hosts surveys on an HTTP endpoint and the page to create surveys on an HTTPS endpoint, so there is no name clash in this particular case.
Markus Says: | |
---|---|
|
DNS Names, Certificates, and SSL in the Surveys Application
In Chapter 1, “The Tailspin Scenario,” you saw how the Surveys application has three different groups of users. This section describes how Tailspin can use Domain Name System (DNS) entries to manage the URLs that each group can use to access the service, and how Tailspin plans to use SSL to protect some elements of the Surveys application.
To make it easy for the Surveys application to meet the requirements outlined earlier, the developers at Tailspin decided to use separate web roles. One web role will contain the subscriber and administrative functionality, while a separate web role will host the surveys themselves. This partitioning of the UI functionality enables Tailspin to scale each web role to support its usage profile independently of the other.
Having multiple web roles in a hosted cloud service affects the choice of URLs that you can use to access the application. Windows Azure assigns a single DNS name (for example, tailspin.cloudapp.net) to a cloud service, which means that different websites within a hosted service must have different port numbers. For example, two websites within Tailspin’s hosted service could have the addresses listed in the following table.
Site A |
Site B |
---|---|
http://tailspin.cloudapp.net:80 |
http://tailspin.cloudapp.net:81 |
Note
You can use DNS CNAME records to map custom domain names to the default DNS names provided by Windows Azure. You can also use DNS A records to map a custom domain name to your service, but the IP address is only guaranteed to remain the same while the application is deployed. If you delete the deployment and then redeploy to the same cloud service, your application will have new IP address, and you will need to change the A record. An IP address is associated with a deployment, not a cloud service. For more information, see the blog post “Windows Azure Deployments and The Virtual IP.”
Because of the specific security requirements of the Surveys application, Tailspin decided to use the following URLs:
The next sections describe each of these.
https://tailspin.cloudapp.net
This HTTPS address uses the default port 443 to access the web role that hosts the administrative functionality for both subscribers and Tailspin. Because an SSL certificate protects this site, it is possible to map only a single custom DNS name. Tailspin plans to use an address such as https://surveys.tailspin.com to access this site.
Poe Says: | |
---|---|
|
http://tailspin.cloudapp.net
This HTTP address uses the default port 80 to access the web role that hosts the public surveys. Because there is no SSL certificate, it is possible to map multiple DNS names to this site. Tailspin will configure a default DNS name such as http://surveys.tailspin.com to access the surveys, and individual tenants can then create their own CNAME entries to map to http://surveys.tailspin.com; for example, http://surveys.adatum.com, http://surveys.tenant2.org, or http://survey.tenant3.co.de.
Poe Says: | |
---|---|
|
Accessing Tailspin Surveys in Different Geographic Regions
Tailspin plans to create separate hosted cloud services to host copies of the Surveys service in different geographic regions. This will enable subscribers to choose where to host their surveys in order to minimize any latency for their users. Each regional version of the public Tailspin Surveys service will be available at a different URL by using a different subdomain. For example, Tailspin could use the following URLs to enable access to versions of Tailspin Surveys hosted in the US, Europe, and the Far East: http://surveys.tailspin.com, http://eusurveys.tailspin.com, and http://fesurveys.tailspin.com. Subscribers could then map their own DNS names to these addresses.
Note
If Tailspin wanted to enable a subscriber to publish a survey that it intends to be available globally, rather than in a specific region, the survey could be hosted on all the Tailspin Surveys cloud services. Tailspin could then use Windows Azure Traffic Manager to route client requests to closest version of Tailspin Surveys. For more information see “Traffic Manager” and Chapter 6, “Maximizing Scalability, Availability, and Performance in the Orders Application,” of the related patterns & practices guide “Building Hybrid Applications in the Cloud on Windows Azure.”
Maintaining Session State
The tenant website uses session state during the survey creation process. The developers at Tailspin considered three options for managing session state.
- Use JavaScript and manage the complete survey creation workflow on the client. Then use AJAX calls to send the complete survey to the server after it’s complete.
- Use the standard built-in Request.Session object to store the intermediate state of the survey while the user is creating it. Because the Tailspin web role will run on several node instances Tailspin cannot use the default, in-memory session state provider, and would have to use another provider such as the session state provider that’s included in Windows Azure Caching. For more information, see “Caching in Windows Azure."
- Use an approach similar to ViewState that serializes and deserializes the workflow state and passes it between the two pages.
You can compare the three options using several different criteria. Which criteria are most significant will depend on the specific requirements of your application.
Simplicity
Something that is simple to implement is usually also easy to maintain. The first option is the most complex of the three, requiring JavaScript skills and good knowledge of an AJAX library. It is also difficult to unit test. The second option is the easiest to implement because it uses the standard ASP.NET Session object. Using the session state provider is simply a matter of “plugging-in” the Windows Azure Caching Service session state provider in the Web.config file. The third option is moderately complex, but you can simplify the implementation by using some of the features in ASP.NET MVC. Unlike the second option, it doesn’t require any server side setup or configuration other than the standard MVC configuration.
Although the second option is easy to implement, it does introduce some potential concerns about the long-term maintenance of the application. The current version of Windows Azure Caching does not support disabling eviction on a cache, so if the cache fills up it could evict session data while the session is still active. The cache uses a least recently used (LRU) policy if it needs to evict items. Tailspin should monitor cache usage and check for the situation where the cache has evicted items from an active session. If this occurs, Tailspin can increase the size of the cache or enable compression to store more data in the existing cache.
Jana Says: | |
---|---|
|
Cost
The first option has the lowest costs because it uses a single POST message to send the completed survey to the server. The second option has moderate costs. If Tailspin chooses to use a Windows Azure Shared Cache, it is easy to determine the cost because Windows Azure bills for the cache explicitly based on the cache size. If Tailspin chooses to use Windows Azure Caching, it is harder to quantify the cost because this type of cache uses a proportion of the memory in Tailspin’s role instances. The third option incurs costs that arise from bandwidth usage; Tailspin can estimate the costs based on the expected number of questions created per day and the average size of the questions.
Jana Says: | |
---|---|
|
Performance
The first option offers the best performance because the client performs almost all the work with no roundtrips to the server until the browser sends a final HTTP POST message containing the complete survey. The second option will introduce a small amount of latency into the application; the amount of latency will depend on the number of concurrent sessions, the amount of data in the session objects, and the latency between the web role and the cache. If Tailspin uses Windows Azure Shared Caching, the latency between the web role and the cache maybe greater than if Tailspin uses Windows Azure Caching. The third option will also introduce some latency because each question will require a round-trip to the server and each HTTP request and response message will include all the current state data.
Scalability
All three options scale well. The first option scales well because it doesn’t require any session state data outside the browser, the second and third options scale well because they are “web-farm friendly” solutions that you can deploy on multiple web roles.
Robustness
The first option is the least robust, relying on client-side JavaScript code. The second option is robust, using a feature that is a standard part of the Windows Azure. The third option is also robust, using easily testable server-side code.
User Experience
The first option provides the best user experience because there are no postbacks during the survey creation process. The other two options require a postback for each question.
Security
The first two options offer good security. With the first option, the browser holds all the survey in memory until the survey creation is complete, and with the second option, the browser just holds a cookie with a session ID while Windows Azure Caching holds the survey data. The third option is not so secure because it simply serializes the data to Base64 without encrypting it. It’s possible that sensitive data could “leak” during the flow between pages.
Tailspin decided to use the second option that uses the session state provider included with Windows Azure Caching. This solution meets Tailspin’s criteria for this part of the Tailspin Surveys application.
Isolating Cached Tenant Data
In addition to using a Windows Azure cache for storing session state, Tailspin also chose to use Windows Azure Caching to cache application data. Tailspin chose to use a co-located Windows Azure cache that uses a proportion of the memory available to the public web site web role instances.
Note
For more information about Windows Azure Caching, see “Overview of Caching in Windows Azure.”
In order to isolate tenant data in the cache that Tailspin Surveys uses to cache frequently used data, such as survey definitions and tenant configuration data, the developers at Tailspin chose to use regions in the Windows Azure cache and assign each tenant its own region. To retrieve an item from the cache the calling code must specify the cache region that contains the required item. This makes it easy for the application to ensure that only data that belongs to a tenant is accessed by that tenant.
Inside the Implementation
Now is a good time to walk through some of the code in the Tailspin Surveys application in more detail. As you go through this section, you may want to download the Visual Studio solution for the Tailspin Surveys application from https://wag.codeplex.com/.
Prioritizing Work in a Worker Role
To enable the worker role in Tailspin Surveys to support prioritizing work from different groups of tenants, the developers at Tailspin introduced some “plumbing” code to launch tasks within a worker role. The following code sample from the Run method of the WorkerRole class in the Tailspin.Workers.Surveys project shows how the Surveys application uses the BatchMultipleQueueHandler class from this plumbing code.
var standardQueue = this.container.Resolve
<IAzureQueue<SurveyAnswerStoredMessage>>
(SubscriptionKind.Standard.ToString());
var premiumQueue = this.container.Resolve
<IAzureQueue<SurveyAnswerStoredMessage>>
(SubscriptionKind.Premium.ToString());
BatchMultipleQueueHandler
.For(premiumQueue, GetPremiumQueueBatchSize())
.AndFor(standardQueue, GetStandardQueueBatchSize())
.Every(TimeSpan.FromSeconds(
GetSummaryUpdatePollingInterval()))
. WithLessThanTheseBatcheIterationsPerCycle(
GetMaxBatchIterationsPerCycle())
.Do(this.container.Resolve
<UpdatingSurveyResultsSummaryCommand>());
Note
The For, AndFor, Every, WithLessThanTheseBatchesPerCycle, and Do methods implement a fluent API for instantiating tasks in the worker role. Fluent APIs help to make the code more legible.
The Run method creates two Windows Azure queues, one to handle messages for standard subscribers and one to handle messages for premium subscribers. The worker role prioritizes processing messages for premium subscribers based on the batch sizes it reads from the service configuration file using the GetPremiumQueueBatchSize and GetStandardQueueBatchSize methods. The worker role also uses the GetSummaryUpdatePollingInterval method to read the service configuration file and set the polling interval for reading messages from the queue, and the GetMaxBatchIterationsPerCycle method to set the maximum number of messages that will be processed in each cycle.
It’s important to limit the maximum number of messages that the worker role process can read from the queue in each cycle. If the code continues reading messages until the queue is empty, but the web role is adding messages faster than the web role can process them, the cycle will never end!
The BatchMultipleQueueHandler class in the worker role enables you to invoke commands of type IBatchCommand<T> by using the Do method. You can invoke these commands on several Windows Azure queues of type IAzureQueue by using the For and AndFor methods, at an interval specified by the Every method. The WithLessThanTheseBatchIterationsPerCycle method limits the number of batches that the task retrieves from the queue before it processes the messages.
The example code you saw above shows the worker role processing a premium and a standard queue, both of which transport SurveyAnswerStoredMessage messages. It processes the messages every ten seconds by using the UpdatingSurveyResultsSummaryCommand class.
Markus Says: | |
---|---|
The design of the BatchMultipleQueueHandler class enables the compiler to check that the two queues and the UpdatingSurveyResultsSummaryCommand class all use the same message type. |
There is also a QueueHandler class that processes messages from a single queue. It has a slightly simpler API; the Do method enables you to invoke commands of type ICommand, the For method identifies a single Windows Azure queue of type IAzureQueue, and the Every method specifies how frequently to process messages.
Note
The tasks that Tailspin runs in the worker role using the task framework described in this chapter include saving the survey responses and calculating the summary statistics. See Chapter 5, “Maximizing Availability, Scalability, and Elasticity,” for descriptions of these tasks.
The BatchMultipleQueueHandler and the Related Classes
This section describes the implementation of the BatchMultipleQueueHandler class and the related classes. The implementation using the QueueHandler class is very similar but runs tasks that implement the simpler ICommand interface instead of the IBatchCommand interface.
Figure 5
Key plumbing types
Figure 5 shows the key types that make up the plumbing code related to the BatchMultipleQueueHandler class that the application uses to prioritize work for premium subscribers. The worker role first invokes the For method in the static BatchMultipleQueueHandler class, which invokes the For method in the BatchMultipleQueueHandler<T> class. The For method returns a BatchMultipleQueueHandler<T> instance that contains a reference to the IAzureQueue<T> instance to monitor.
The plumbing code identifies the queue by name and associates it with a message type that derives from the AzureQueueMessage type. For example, both the Standard and Premium queues handle SurveyAnswerStoredMessage messages. The following code example shows how the static For method in the BatchMultipleQueueHandler class instantiates a BatchMultipleQueueHandler<T> instance and invokes the For method, passing the required batch size as a parameter.
using Tailspin.Web.Survey.Shared.Stores.AzureStorage;
public static class BatchMultipleQueueHandler
{
public static BatchMultipleQueueHandler<T>
For<T>(IAzureQueue<T> queue, int batchSize)
where T : AzureQueueMessage
{
return BatchMultipleQueueHandler<T>.For
(queue, batchSize);
}
}
Next, the worker role invokes the AndFor method for each additional queue that transports messages. The following code sample shows both the For and the AndFor methods of the BatchMultipleQueueHandler<T> class.
public static BatchMultipleQueueHandler<T> For
(IAzureQueue<T> queue, int batchSize)
{
if (queue == null)
{
throw new ArgumentNullException("queue");
}
batchSize = Math.Max(1, batchSize);
return new BatchMultipleQueueHandler<T>(queue, batchSize);
}
public BatchMultipleQueueHandler<T> AndFor
(IAzureQueue<T> queue, int batchSize)
{
if (queue == null)
{
throw new ArgumentNullException("queue");
}
batchSize = Math.Max(1, batchSize);
this.queuesConfiguration.Add
(QueueBatchConfiguration.BuildConfig(queue, batchSize));
return this;
}
Next, the worker role invokes the Every method of the BatchMultipleQueueHandler<T> object to specify how frequently the task should be run. Then it invokes the WithLessThanTheseBatchIterationsPerCycle method to limit the number of batches to process in each cycle.
Finally, the worker role invokes the Do method of the BatchMultipleQueueHandler<T> object, passing an IBatchCommand object that identifies the command that the plumbing code should execute on each message in the queue. The following code example shows how the Do method uses the Task.Factory.StartNew method from the Task Parallel Library (TPL) to execute the PreRun, ProcessMessages, and PostRun methods on the queue at the requested interval.
Use Task.Factory.StartNew in preference to ThreadPool.QueueUserWorkItem to ensure that your application can maximize performance on any system on which it will run.
public virtual void Do(IBatchCommand<T> batchCommand)
{
Task.Factory.StartNew(
() =>
{
while (true)
{
this.Cycle(batchCommand);
}
},
TaskCreationOptions.LongRunning);
}
protected void Cycle(IBatchCommand<T> batchCommand)
{
try
{
batchCommand.PreRun();
int batches = 0;
bool continueProcessing;
do
{
continueProcessing = false;
foreach (var queueConfig in this.queuesConfiguration)
{
var messages = queueConfig.Queue
.GetMessages(queueConfig.BatchSize);
GenericQueueHandler<T>.ProcessMessages(
queueConfig.Queue, messages, batchCommand.Run);
continueProcessing |= messages.Count()
>= queueConfig.BatchSize;
}
batches++;
}
while (continueProcessing && batches
< maxBatchesPerCycle);
batchCommand.PostRun();
this.Sleep(this.interval);
}
catch (TimeoutException ex)
{
TraceHelper.TraceWarning(ex.TraceInformation());
}
catch (Exception ex)
{
// No exception should get here -
// we don't want the handler to stop
// (we log it as ERROR)
TraceHelper.TraceError(ex.TraceInformation());
}
}
The Cycle method repeatedly pulls messages for processing from the queue, up to the number specified as the batch size in the For and AndFor methods, in a single transaction; until there are no more messages left or the maximum batches per cycle is reached.
Markus Says: | |
---|---|
By configuring one queue to use a larger batch size, you can ensure that the worker role processes messages in that queue faster than other queues. In addition, reading messages from queues in batches can reduce your costs because it reduces the number of storage operations. |
The following code example shows the ProcessMessages method in the GenericQueueHandler class that performs the actual message processing.
protected static void ProcessMessages(IAzureQueue<T> queue,
IEnumerable<T> messages, Func<T, bool> action)
{
...
foreach (var message in messages)
{
var allowDelete = false;
var corruptMessage = false;
try
{
allowDelete = action(message);
}
catch (Exception ex)
{
TraceHelper.TraceError(ex.TraceInformation());
allowDelete = false;
corruptMessage = true;
}
finally
{
if (allowDelete || (corruptMessage
&& message.GetMessageReference().DequeueCount > 5))
{
queue.DeleteMessage(message);
}
}
}
}
This method uses the action parameter to invoke the custom command on each message in the queue; if this fails it logs the error. Finally, the method checks for poison messages by looking at the DequeueCount property of the message; if the application has tried more than five times to process the message, the method deletes the message.
Note
Instead of deleting poison messages, you should send them to a dead message queue for analysis and troubleshooting.
Using MVC Routing Tables
The request routing implementation in the Tailspin Surveys application uses a combination of ASP.NET routing tables and MVC areas to identify the subscriber and map requests to the correct functionality within the application.
The following code example shows how the public Surveys Web site uses routing tables to determine which survey to display based on the URL.
using System.Web.Mvc;
using System.Web.Routing;
public static class AppRoutes
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Home",
string.Empty,
new { controller = "Surveys", action = "Index" });
routes.MapRoute(
"ViewSurvey",
"survey/{tenant}/{surveySlug}",
new { controller = "Surveys", action = "Display" });
routes.MapRoute(
"ThankYouForFillingTheSurvey",
"survey/{tenant}/{surveySlug}/thankyou",
new { controller = "Surveys", action = "ThankYou" });
}
}
The code extracts the tenant name and survey name from the URL and passes them to the appropriate action method in the SurveysController class. The following code example shows the Display action method that handles HTTP GET requests.
[HttpGet]
public ActionResult Display(string tenant,
string surveySlug)
{
var surveyAnswer = CallGetSurveyAndCreateSurveyAnswer(
this.surveyStore, tenant, surveySlug);
var model =
new TenantPageViewData<SurveyAnswer>(surveyAnswer);
if (surveyAnswer != null)
{
model.Title = surveyAnswer.Title;
}
return this.View(model);
}
If the user requests a survey using a URL with a path value of /survey/adatum/launch-event-feedback, the value of the tenant parameter will be “Adatum” and the value of the surveySlug parameter will be “launch-event-feedback.” The Display action method uses the parameter values to retrieve the survey definition from the store, populate the model with this data, and pass the model to the view that renders it to the browser.
Markus Says: | |
---|---|
There is also a Display action to handle HTTP POST requests. This controller action is responsible for saving the data from a filled out survey. |
The Subscriber website is more complex because, in addition to enabling subscribers to design new surveys and analyze survey results, it must handle authentication and onboarding new subscribers. Because of this complexity it uses MVC areas as well as a routing table. The following code from the AppRoutes class in the TailSpin.Web project shows how the application maps top level requests to the controller classes that handle onboarding and authentication.
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"OnBoarding",
string.Empty,
new { controller = "OnBoarding", action = "Index" });
routes.MapRoute(
"FederationResultProcessing",
"FederationResult",
new { controller = "ClaimsAuthentication",
action = "FederationResult" });
routes.MapRoute(
"FederatedSignout",
"Signout",
new { controller = "ClaimsAuthentication",
action = "Signout" });
}
...
}
The application also defines an MVC area for the core survey functionality. MVC applications register areas by calling the RegisterAllAreas method. In the TailSpin.Web project you can find this call in the Application_Start method in the Global.asax.cs file. The RegisterAllAreas method searches the application for classes that extend the AreaRegistration class, and then it invokes the RegisterArea method. The following code example shows a part of this method in the SurveyAreaRegistration class.
Markus Says: | |
---|---|
MVC areas enable you to group multiple controllers together within the application, making it easier to work with large MVC projects. Each MVC area typically represents a different functional area within the application. |
public override void RegisterArea(
AreaRegistrationContext context)
{
context.MapRoute(
"MySurveys",
"survey/{tenant}",
new { controller = "Surveys", action = "Index" });
context.MapRoute(
"NewSurvey",
"survey/{tenant}/newsurvey",
new { controller = "Surveys", action = "New" });
context.MapRoute(
"NewQuestion",
"survey/{tenant}/newquestion",
new { controller = "Surveys", action = "NewQuestion" });
context.MapRoute(
"AddQuestion",
"survey/{tenant}/newquestion/add",
new { controller = "Surveys", action = "AddQuestion" });
...
}
Notice how all the routes in this routing table include the tenant name that MVC passes as a parameter to the controller action methods.
Web Roles in Tailspin Surveys
To implement the two different websites within a single hosted cloud service, the developers at Tailspin defined two web roles in the solution. The first website, named TailSpin.Web, is an MVC project that handles the administrative functionality within the application. This website requires authentication and authorization, and users access it using HTTPS. The second website, named Tailspin.Web.Survey.Public, is an MVC project that handles users filling out surveys. This website is public, and users access it using HTTP.
The following code example shows the contents of an example ServiceDefinition.csdef file and the definitions of the two web roles in Tailspin Surveys:
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="Tailspin.Cloud" xmlns=...>
<WebRole name="Tailspin.Web"
enableNativeCodeExecution="true">
<Sites>
<Site name="Web">
<Bindings>
<Binding name="HttpsIn" endpointName="HttpsIn" />
</Bindings>
</Site>
</Sites>
<Certificates>
<Certificate name="localhost"
storeLocation="LocalMachine" storeName="My" />
</Certificates>
<Endpoints>
<InputEndpoint name="HttpsIn" protocol="https"
port="443" certificate="localhost" />
</Endpoints>
...
</WebRole>
<WebRole name="Tailspin.Web.Survey.Public">
<Sites>
<Site name="Web">
<Bindings>
<Binding name="HttpIn" endpointName="HttpIn" />
</Bindings>
</Site>
</Sites>
<Endpoints>
<InputEndpoint name="HttpIn" protocol="http"
port="80" />
</Endpoints>
...
</WebRole>
<WorkerRole name="Tailspin.Workers.Surveys">
...
</WorkerRole>
</ServiceDefinition>
This example ServiceDefinition.csdef file does not exactly match the file in the downloadable solution, which uses a different name for the SSL certificate.
Note
Remember, you may want to use different SSL certificates when you are testing the application using the local compute emulator. You must make sure that the configuration files reference the correct certificates before you publish the application to Windows Azure. For more information about managing the deployment, see Chapter 3, “Moving to Windows Azure Cloud Services,” in the guide “Moving Applications to the Cloud.”
In addition to the two web role projects, the solution also contains a worker role project and a library project named TailSpin.Web.Survey.Shared that contains code shared by the web and worker roles. This shared code includes the model classes and the data access layer.
Implementing Session Management
The Surveys application must maintain some state data for each user as they design a survey. This section describes the design and implementation of user state management in the Surveys application.
The following code examples shows how the action methods in the SurveysController controller class in the TailSpin.Web project use the MVC TempData property to cache the survey definition while the user is designing a new survey. Behind the scenes, the TempData property uses the ASP.NET session object to store cached objects.
The New method that handles GET requests, shown here, is invoked when a user navigates to the New Survey page.
[HttpGet]
public ActionResult New()
{
var cachedSurvey = (Survey)this.TempData[CachedSurvey];
if (cachedSurvey == null)
{
cachedSurvey = new Survey(); // First time to the page
}
var model = this.CreateTenantPageViewData(cachedSurvey);
model.Title = "New Survey";
this.TempData[CachedSurvey] = cachedSurvey;
return this.View(model);
}
The NewQuestion method is invoked when a user chooses the Add Question link on the Create a new survey page. The method retrieves the cached survey that the New method created, ready to display it to the user.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult NewQuestion(Survey contentModel)
{
var cachedSurvey = (Survey)this.TempData[CachedSurvey];
if (cachedSurvey == null)
{
return this.RedirectToAction("New");
}
cachedSurvey.Title = contentModel.Title;
this.TempData[CachedSurvey] = cachedSurvey;
var model = this.CreateTenantPageViewData(new Question());
model.Title = "New Question";
return this.View(model);
}
The AddQuestion method is invoked when a user chooses the Add to survey button on the Add a new question page. The method retrieves the cached survey and adds the new question, then updates the survey stored in the session.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddQuestion(Question contentModel)
{
var cachedSurvey = (Survey)this.TempData[CachedSurvey];
if (!this.ModelState.IsValid)
{
this.TempData[CachedSurvey] = cachedSurvey;
var model = this.CreateTenantPageViewData(
contentModel ?? new Question());
model.Title = "New Question";
return this.View("NewQuestion", model);
}
if (contentModel.PossibleAnswers != null)
{
contentModel.PossibleAnswers =
contentModel.PossibleAnswers.Replace("\r\n", "\n");
}
cachedSurvey.Questions.Add(contentModel);
this.TempData[CachedSurvey] = cachedSurvey;
return this.RedirectToAction("New");
}
The New method that handles POST requests is invoked when a user chooses the Create button on the Create a new survey page. The method retrieves the completed, cached survey, saves it to persistent storage, and removes it from the session.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult New(Survey contentModel)
{
var cachedSurvey = (Survey)this.TempData[CachedSurvey];
if (cachedSurvey == null)
{
return this.RedirectToAction("New");
}
if (cachedSurvey.Questions == null ||
cachedSurvey.Questions.Count <= 0)
{
this.ModelState.AddModelError("ContentModel.Questions",
string.Format(CultureInfo.InvariantCulture,
"Please add at least one question to the survey."));
}
contentModel.Questions = cachedSurvey.Questions;
if (!this.ModelState.IsValid)
{
var model = this.CreateTenantPageViewData(contentModel);
model.Title = "New Survey";
this.TempData[CachedSurvey] = cachedSurvey;
return this.View(model);
}
contentModel.Tenant = this.TenantName;
try
{
this.surveyStore.SaveSurvey(contentModel);
}
catch (DataServiceRequestException ex)
{
...
}
this.TempData.Remove(CachedSurvey);
return this.RedirectToAction("Index");
}
Markus Says: | |
---|---|
Tailspin use the TempData property instead of working with the ASP.NET Session object directly because the entries in the TempData dictionary live only for a single request, after which they’re automatically removed from the session. This makes it easier to manage the contents of the session. |
Tailspin just needed to modify the configuration settings in the Surveys application to change from using the default, in-memory, ASP.NET session state provider to using the session state provider that uses Windows Azure Caching. No application code changes were necessary. The following sections describe the configuration changes Tailspin made.
Configuring a Cache in Windows Azure Caching
Tailspin uses the ASP.NET 4 Caching Session State Provider in the tenant web role. This requires Tailspin to configure Windows Azure Caching in the project, and Tailspin chose to use a co-located cache that uses a proportion of the web role’s memory. You can configure the settings for this type of cache using the role properties in Visual Studio.
The following sample shows the part of the service configuration file where the cache configuration is stored. The value for NamedCaches is the default set by the SDK; it allows you to change the cache settings while the application is running simply by editing the configuration file.
<ServiceConfiguration serviceName="Tailspin.Cloud" ... >
<Role name="Tailspin.Web">
<Instances count="1" />
<ConfigurationSettings>
...
<Setting
name="Microsoft.WindowsAzure.Plugins
.Caching.NamedCaches"
value="{"caches":[{"name"
:"default","policy"
:{"eviction"
:{"type":0},"expiration"
:{"defaultTTL"
:10,"isExpirable"
:true,"type":1},"
serverNotification"
:{"isEnabled"
:false}},"secondaries":0}]}" />
<Setting
name="Microsoft.WindowsAzure.Plugins
.Caching.DiagnosticLevel"
value="1" />
<Setting name="Microsoft.WindowsAzure.Plugins
.Caching.Loglevel"
value="" />
<Setting name="Microsoft.WindowsAzure.Plugins
.Caching.CacheSizePercentage"
value="30" />
<Setting name="Microsoft.WindowsAzure.Plugins
.Caching.ConfigStoreConnectionString"
value="UseDevelopmentStorage=true" />
</ConfigurationSettings>
...
</Role>
<Role name="Tailspin.Web.Survey.Public">
...
</Role>
<Role name="Tailspin.Workers.Surveys">
...
</Role>
</ServiceConfiguration>
This example shows how to configure a default cache that use 30% of the available memory in the Tailspin.Web web role instances. It uses the local storage emulator to store the cache’s runtime state, and you must change this to use a Windows Azure storage account when you deploy the application to Windows Azure.
Note
Tailspin used NuGet to add the required assemblies and references to the Tailspin.Web.Survey.Shared project.
Configuring the Session State Provider in the TailSpin.Web Application
The final changes that Tailspin made were to the Web.config file in the TailSpin.Web project. The following example shows these changes.
<configSections>
...
<section name="dataCacheClients"
type="Microsoft.ApplicationServer
.Caching.DataCacheClientsSection,
Microsoft.ApplicationServer.Caching.Core"
allowLocation="true" allowDefinition="Everywhere"/>
</configSections>
...
<dataCacheClients>
<tracing sinkType="DiagnosticSink"
traceLevel="Error" />
<dataCacheClient name="default"
maxConnectionsToServer="5">
<autoDiscover isEnabled="true"
identifier="Tailspin.Web" />
</dataCacheClient>
</dataCacheClients>
...
<system.web>
<sessionState mode="Custom"
customProvider="AppFabricCacheSessionStoreProvider">
<providers>
<add name="AppFabricCacheSessionStoreProvider"
type="Microsoft.Web.DistributedCache
.DistributedCacheSessionStateStoreProvider,
Microsoft.Web.DistributedCache"
cacheName="default" useBlobMode="false"
dataCacheClientName="default" />
</providers>
</sessionState>
...
</system.web>
The sessionState section configures the application to use the default cache provided by the Windows Azure Caching session state provider.
Caching Frequently Used Data
The public website frequently accesses survey definitions and tenant data in read-only mode to display surveys to respondents. To reduce latency the application attempts to use cached versions of this data if it is available.
The following code sample shows the TenantCacheHelper class that ensures that each tenant has its own region in the cache in order to isolate its data from other tenants. The sample also shows how the RemoveAllFromCache method removes all the cache entries that belong to a single tenant.
internal static class TenantCacheHelper
{
private static readonly DataCacheFactory CacheFactory;
private static readonly IRetryPolicyFactory
RetryPolicyFactory;
...
internal static void AddToCache<T>(string tenant,
string key, T @object) where T : class
{
GetRetryPolicy().ExecuteAction(() =>
{
DataCache cache = CacheFactory.GetDefaultCache();
if (!cache.GetSystemRegions().Contains(
tenant.ToLowerInvariant()))
{
cache.CreateRegion(tenant.ToLowerInvariant());
}
cache.Put(key.ToLowerInvariant(), @object,
tenant.ToLowerInvariant());
});
}
internal static T GetFromCache<T>(string tenant,
string key, Func<T> @default) where T : class
{
return GetRetryPolicy().ExecuteAction<T>(() =>
{
var result = default(T);
var success = false;
DataCache cache = CacheFactory.GetDefaultCache();
result = cache.Get(key.ToLowerInvariant(),
tenant.ToLowerInvariant()) as T;
if (result != null)
{
success = true;
}
else if (@default != null)
{
result = @default();
if (result != null)
{
AddToCache(tenant.ToLowerInvariant(),
key.ToLowerInvariant(), result);
}
}
TraceHelper.TraceInformation(
"cache {2} for {0} [{1}]",
key, tenant, success ? "hit" : "miss");
return result;
});
}
internal static void RemoveFromCache(string tenant,
string key)
{
GetRetryPolicy().ExecuteAction(() =>
{
DataCache cache = CacheFactory.GetDefaultCache();
cache.Remove(key.ToLowerInvariant(),
tenant.ToLowerInvariant());
});
}
internal static void RemoveAllFromCache(string tenant)
{
GetRetryPolicy().ExecuteAction(() =>
{
DataCache cache = CacheFactory.GetDefaultCache();
cache.RemoveRegion(tenant.ToLowerInvariant());
});
}
...
}
The following code sample shows how the SurveyStore class uses the TenantCacheHelper class to maintain survey definitions in the cache.
public class SurveyStore : ISurveyStore
{
...
public void SaveSurvey(Survey survey)
{
...
TenantCacheHelper.AddToCache(survey.Tenant,
slugName, survey);
...
}
public void DeleteSurveyByTenantAndSlugName(
string tenant, string slugName)
{
...
TenantCacheHelper.RemoveFromCache(tenant, slugName);
...
}
public Survey GetSurveyByTenantAndSlugName(string tenant,
string slugName, bool getQuestions)
{
...
return this.CacheEnabled ?
TenantCacheHelper.GetFromCache(tenant,
slugName, resolver) : resolver();
...
}
...
}
More Information
For more information about Windows Azure multi-tenant application design, see “Designing Multitenant Applications on Windows Azure.”
For more information about routing in ASP.NET, see “ASP.NET Routing” on MSDN.
For more information about using CNAME entries in DNS, see the post “Custom Domain Names in Windows Azure” on Steve Marx’s blog.
For a description of the different caching options available in Windows Azure, see “Caching in Windows Azure.”
For more information about Windows Azure resource provisioning, see “Provisioning Windows Azure for Web Applications.”
For more information about the hard and soft limits in Windows Azure, see “Best Practices for the Design of Large-Scale Services on Windows Azure Cloud Services” on MSDN.
For more information about fluent APIs, see the entry for “Fluent interface” on Wikipedia.
For information about the Task Parallel Library, see “Task Parallel Library” on MSDN.
For information about the advantages of using the Task Parallel library instead of working with the thread pool directly, see the following:
- The article “Optimize Managed Code for Multi-Core Machines” in MSDN Magazine.
- The blog post “Choosing Between the Task Parallel Library and the ThreadPool” on the Parallel Programming with .NET blog.