CAS is a Single SignOn solution. Internally the function of CAS is to create, update, and delete a set of objects it calls "Tickets" (a word borrowed from Kerberos). A Logon Ticket object is created to hold the Netid when a user logs on to CAS. A partially random string is generated to be the login ticket-id and is sent back to the browser as a Cookie and is also used as a "key" to locate the logon ticket object in a table. Similarly, CAS creates Service Tickets to identity a logged on user to an application that uses CAS authentication.
CAS data or "state" only consists of its configuration parameters, its saved tickets, and a temporary "flow" state maintained between the time it writes the login form to the screen and the time the user submits a userid and password. Ticket objects are the only important data to save, restore, or share.
A standalone CAS server stores ticket objects in memory, but when you add a second CAS server to the network for reliability, then you have to share the ticket objects either by sharing tables in a database or by configuring one of several packages that replicate Java objects over a network.
Four years ago Yale implemented a "High Availability" CAS cluster using JBoss Cache to replicate tickets. After that, the only CAS crashes were caused by failures of the ticket replication mechanism. We considered replacing JBoss, but there is a more fundamental problem here. It should not be possible for any possible failure of the replication mechanism to crash CAS. However, given the design of all the existing replication technologies, CAS cannot function properly if they fail. So replacing one magic black box of code with another, hoping the second is more reliable, is less desirable than fixing the original design problem.
CAS depends on replication because it makes no assumptions about the network Front End device that distributes Web requests among servers. That made sense 10 years ago, but today these devices are much smarter. At Yale, and I suspect at many institutions, the Front End is a BIG-IP F5 device. It can be programmed with iRules, and it is fairly simple to create iRules that understand the basic CAS protocol. If the CAS servers are properly configured and the F5 is programmed, then requests from the browser for a new Service Ticket or requests from an application to validate the Service Ticket ID can be routed to the CAS server that created the Service Ticket and not just to some random server in the cluster. After than, ticket replication is a much simpler and can be a much more reliable process.
General object replication systems are necessary for shopping cart applications that handle thousands of concurrent users spread across a number of machines. E-Commerce applications don't have to worry about running when the network is generally sick or there is no database in which to record the transactions, but CAS is a critical infrastructure component that has to be up at all times.
The CAS component that holds and replicates ticket objects is called the TicketRegistry. CushyTicketRegistry ("Cushy") is a new option you can configure to a CAS server. Cushy does useful things for a single standalone server, but it can also be configured to support a cluster of servers behind a modern programmable Front End device. It is explicitly not a general purpose object replication system. It handles CAS tickets. Because it is a easy to understand single Java source file with no external dependencies, it can be made incrementally smarter about ticket objects and how to optimally manage them when parts of the network fail and when full service is restored.
CushyTicketRegistry cannot ever crash CAS. It completely separates ticket back with CAS function. If there is a problem it will periodically retry replication until the problem is fixed, but that is completely separate from the rest of CAS function.
"Cushy" stands for "Clustering Using Serialization to disk and Https transmission of files between servers, written by Yale". This summarizes what it is and how it works.
Cushy was designed for small and medium sized CAS installations. We have heard of CAS users with OutOfMemory situations, but it is not clear if that is caused by bug if if there actually are millions of tickets. You can view your own statistics, use the JUnit test to create similar sized data, and generate your own benchmarks. If the results are not satisfactory, use another TicketRegistry option.
Cushy writes tickets to a disk file. Some organizations install a CAS option that stores passwords in the ticket. If the ticket is written to disk, then you have to protect the disk file and directory. If this is unsatisfactory, use another TicketRegistry option.
The Standalone Server
For a simple single standalone CAS server, the standard choice is the DefaultTicketRegistry class which keeps the tickets in an in memory Java table keyed by the ticket id string. Suppose you simply change the class name from DefaultTicketRegistry to CushyTicketRegistry (and add a few required parameters described later). Cushy was based on the DefaultTicketRegistry code, so everything works the same as before until you have to restart CAS for any reason. Since the DefaultTicketRegistry only has an in memory table, all the ticket objects are lost when the application restarts and users have to login again. Cushy detects the shutdown and saves all the ticket objects to a file on disk, using a single Java writeObject statement on the entire collection. Unless that file is deleted while CAS is down, then when CAS restarts Cushy reloads all the tickets from that file into memory and restores all the CAS state from before the shutdown. No user even notices that CAS restarted unless they tried to access CAS during the restart.
The number of tickets CAS holds grows during the day and shrinks over night. The largest number occurs late in the day. At Yale there are fewer than 20,000 ticket objects in CAS memory, andCushy can write all those tickets to disk in less than a second generating a file smaller than 3 megabytes. This is such a small amount of overhead that Cushy can be proactive.
So to take the next logical step, start with the previous ticketRegistry.xml configuration and duplicate the statements that currently run the RegistryCleaner every few minutes. The new statements will call the "timerDriven" method of the ticketRegistry object (Cushy) every few minutes. Now Cushy will not wait for shutdown but will back up the ticket objects regularly just in case the CAS machine crashes without shutting down normally. When CAS restarts, it can load a fairly current copy of the ticket objects which will satisfy the 99.9% of the users who did not login in the last minutes before the crash.
At this point, the next step should be obvious. Can we turn "last few minutes" into "last few seconds". Creating a complete backup of the entire set of tickets is not terribly expensive, but it is not something you want to do continuously. So Cushy can be configured to create "incremental" files between every full checkpoint backup. The incremental file contains all the changes accumulated since the last full checkpoint, so you do not have a bunch of files to process in order. Just apply the last full checkpoint and then the incremental file on top of it.
The full checkpoint takes a few seconds to build, the incremental takes a few milliseconds. So you run the full backup every (say) 5 minutes and you run an incremental every (say) 10 seconds.
The checkpoint and incremental files are ordinary sequential binary files on disk. Cushy writes a new file and then swaps it for the old file, so other programs authorized to access the directory can freely open or copy the files while CAS is running. This is useful because occasionally a computer that crashes cannot just reboot. Since the two files on disk represent all the data that needs to be saved and restored, if you want to prepare for disaster recovery you may want to periodically copy the files to a location far away, in another data center or in the cloud. Cushy doesn't do this itself, but you can easily write a shell script or Pearl or Python program to do it. Since they are normal files, you can copy them with SFTP or any other file utility.
Rethink the Cluster Design
Before you configure a cluster, remember that today a server is typically a virtual machine that is not bound to any particular physical hardware. Ten years ago moving a service to a backup machine involved manual work that took time. Today there is VM infrastructure and automated monitoring and control tools. A failed server can be migrated and restarted automatically or with a few commands. If you can get the CAS server restarted fast enough that almost nobody notices, then you have solved the problem that clustering was originally designed to solve. All you need is Cushy's ability to save and restore the tickets.
However, VMs are also cheap and you may prefer to run more than one CAS server. In this case, Cushy offers and entirely different approach to CAS clustering. This new approach is driven by new technology that has been added to machine rooms since the original CAS cluster design was developed.
The cluster will still run in a modern VM infrastructure. This means that individual CAS node outages should be measured in minutes instead of hours.
In any clustered application, all requests go to a single network address ("https://secure.its.yale.edu/cas") that points to a Front End machine. Ten years ago that Front End was dumb and simply distributed the requests round-robin across the set of back end servers. Today, Front End machines, such as the BIG-IP F5, are much smarter and they can be programmed with enough understanding of the CAS protocol so that they only round robin the initial login of new users. After that, if a request arrives at the CAS virtual IP address, then the login ticketid is in the CASTGC Cookie HTTP header, the Service Ticket ID is in the ticket= parameter in the query string of a validate request, or the Proxy ticket ID is in the pgt= parameter of the query string in a /cas/proxy request. CAS has always had the ability to identify the node that created the ticket by a suffix added to all ticket ID strings. Cushy adds a formal methodology to enforce this.
Cushy can be configured node by node, but Yale Production Services did not want to configure machines individually. So Cushy adds a configuration class to which you configure the cluster. Actually, you configure every CAS cluster you have in your enterprise (desktop sandbox, development, test, stress test, production, ...). When CAS starts up the configuration class figures out which cluster this machine is a member of, and it configures that cluster and this machine. If also feeds a "ticket ID suffix" string to the CAS components that generate ticket IDs so that the Front End will route tickets properly.
How does Cushy handle clustering? At startup, it creates a "secondary" TicketRegistry that will contain a shadow copy of ticket for each of the other nodes in the cluster. However, as long as the network and nodes are healthy, Cushy only needs access to or a copy of the full checkpoint and incremental file for each node in the network. It does not open the files to restore tickets until there is a failure.
The file names are created from the node names of the CAS servers, so they can all coexist in the same directory. The simplest Cushy communication option is "SharedDisk". When this is chosen, Cushy expects that the other nodes are writing their full backup and incremental files to the same disk directory it is using. If Cushy receives a request that the Front End should have sent to another node, then Cushy assumes some failure has occurred, loads the other node's tickets into memory, and processes the request on behalf of the other node.
Of course you are free to implement SharedDisk with an actual file server or NAS, but technically Cushy doesn't know or care how the files got to the hard drive. So if you don't like real shared disk technology, you can write a shell script somewhere to wake up periodically and copy the files between machines using SFTP or whatever file transfer mechanism you like to use. You could also put the 3 megabyte file on the Enterprise Service Bus if you prefer architecture to simplicity.
However, Cushy provides a built-in data transfer solution based on simple HTTPS GET requests. After all, CAS runs on a Web server, and they are very good about sending the current copy of small files over the network to clients. Everyone understands how an HTTP GET works. So unless you configure "SharedDisk", Cushy running in cluster mode uses HTTPS GET to retrieve a copy of the most recent full checkpoint or incremental file from every other node in the cluster and puts the copy in its work directory on the local hard disk.
Everything that can go wrong will go wrong. It is easy to plan for a server crashing. However, suppose you maintain multiple redundant data centers and the fiber connection is broken between centers, or a main router breaks somewhere in the network. Everything is up, but some machines cannot talk to each other. The Front End may believe a CAS server is down while other CAS servers can get to it, or the Front End may be able to talk to all servers but they may not be able to talk to each other. What about disaster recovery?
The other CAS clustering techniques (JBoss Cache, Ehcache, Memcached) all use complex mechanisms to detect failure, to manage the outage, and to merge results when communication is reestablished. How exactly do they work? What will they do in every possible failure scenario? These systems are so complex and powerful that you have to assume they will eventually do everything right because you cannot plausibly understand how they work. If the problem really was that big, there would be no other choice.
However, CAS tickets aren't really that complex. The requirements can be met by two simple steps: convert the objects to a file on disk, then transmit the file from node to node using HTTPS GET. There is no magic black box here that claims to solve all your problems if you don't look under the covers. This is a solution you can understand and own and plan. Yes it is a little less efficient than the more sophisticated packages, but the problem is so small that efficiency is not required and simplicity is more valuable. This document still has to fill in a little more detail, and a moderately skilled Java programmer can read the source.
CAS Ticket Objects Need to be Fixed
Now the bad news. Current CAS has some bugs. It was not written "properly" to work with the various ticket replication mechanisms. It has worked well enough in the past, but CAS 4 introduces new features and in the future it may not behave as expected. It is not possible to fix everything in the TicketRegistry. A few changes may need to be made in the CAS Ticket classes. So Cushy does not fix the bugs itself, but it does eliminate the false reliance of "the magic black box of off the shelf software" that people imagined was going to do more than it could reasonably be expected to do.
1) Any system that seeks to replicate tickets has a concurrency problem if there are multiple threads (like the request threads maintained by any Web Server) that can change the content of an object while another thread has triggered replication of the object. CAS has some collections in its TicketGrantingTicket object that can be changed by one Web request while another request is trying to serialize the ticket for replication to another system. CAS 3 was sloppy about this. CAS 4 added the "synchronized" attribute to methods so at least the CAS API is protected from threading problems. However, when tickets get passed to a black box cache mechanism for replication, then under the covers they are "serialized" to a stream of bytes, and serialization is not synchronized with updates unless you write a trivial change to protect it, and that change is not yet in CAS 4.0. As a result, any of the ticket replication technologies has a very, very small chance of throwing a ConcurrentModificationException. Cushy doesn't solve this problem yet, because it doesn't change the Ticket classes that have the bug, but it does provide a small amount of transparent pure Java code where a fix can be validated.
2) Any system that replicates tickets using serialization gets not just the object they are trying to serialize but also a copy of any other objects it points to. In CAS a Service or Proxy ticket points to a TGT, and when you try to serialize one of them you get a copy of the TGT dragged along under the covers and then recreated at the other end when the data is turned back into a Ticket object. That didn't matter in CAS 3 because the TGT didn't change in any important way after it was created. This may not be sufficient in CAS 4 when people start to add additional factors of authentication to an existing logon.
3) It is not possible to fix the previous problem in the TicketRegistry alone because the Ticket classes do not expose a method that allows the Registry to reconnect the copy of the Proxy or Service Ticket to the TGT already in the registry. Cushy mostly "solves" the problem because every full checkpoint (every 5 minutes or so) fixes the broken pointers, but Cushy is still stuck with the problem in tickets added by incrementals between full checkpoints. It would be better to modify the Ticket classes so that even tickets added by incrementals could be reconnected to the real TGT instead of their private copy. In all the other replication systems, the problem is never solved and generally cannot be solved (because they hide the moment when a ticket is replicated).
The big difference here is that Cushy is designed 100% to satisfy the needs of CAS, and so we can discuss and fix those specific problems. The larger off the shelf generic libraries provide no place to fix problems specific to CAS and up to this point nobody seems to have noticed or fixed the problems.
Why another Clustering Mechanism?
One solution is to share all the ticket objects and their associated components in database tables using JPA. JPA is the standard Java mechanism for mapping objects to tables. It is an enormously powerful tool for ordinary Web applications. It is a possible solution, but CAS doesn't have a database problem:
- CAS tickets all timeout after a number of hours. They have no need for long term persistence.
- There are no meaningful SQL operations in CAS. Nobody will generate reports based on tickets.
- CAS has no transactional structure or need for a conventional commit operation.
Most importantly, having created a cluster for availability, JPA now makes the database a single point of failure. Configuring a database for 24x7x366 availability and guaranteeing that it comes up before CAS places a significant and unnecessary burden on most CAS installations.
The alternative is to use one of several "cache" libraries (Ehcache, JBoss Cache, Memcached). They create the impression of a large pool of ordinary Java objects shared by all the CAS servers. Any change made to objects in the pool are automatically and transparently replicated to all the other servers. These systems also solve very large problems and they can have very complicated configurations with exotic network parameters.
A common problem with both JPA and the generic "cache" solutions is that they integrate into CAS "inline". JPA is driven by annotations that are added to the Java source of the Ticket classes, but under the covers it dynamically generates code that it transparently "weaves" into the classes. The cache systems intercept TicketRegistry operations such as addTicket to make sure that copies of the tickets are moved to some network communications queue. In either case we have observed that when things get bad, when the network is sick or something generates an unexpected error, the problem can "back up" from the replication mechanism back into the TicketRegistry and then into CAS itself.
In Cushy, the only connection between the CAS mainline function (the part of CAS that responds to Web requests and services users and applications) is that references to objects are occasionally copied from one in memory collection to another. Separately, on a timer driven basis collections of objects are periodically written to disk with a single Java writeObject statement. Separately, on a network request driven basis, copies of those files are then send to other CAS nodes. If there is a network problem, the HTTP GET fails with an I/O error, this operation is aborted completely, then the servers try again 10 or 15 seconds later. Each step places an absolute boundary between itself and the other steps. None of them can interfere with CAS mainline services. There are no queues or operations to clog and back up into the running code.
Comparison of Cushy and previous cluster technologies:
- The other CAS cluster mechanisms are designed so the CAS servers all share a common pool of tickets. The problem then is to get a copy of a new or modified ticket to all the other servers before they receive a request that requires it. A Cushy cluster is really a connected group of standalone CAS servers. The user may initially login to a randomly chosen server, but once she logs on the Front End routes all requests for that user to that particular server.
- The other CAS cluster mechanisms try to replicate individual tickets. Cushy replicates the ticket registry as a whole, either as a full backup file or as cumulative incremental changes.
- Other mechanisms are driven by individual addTicket or deleteTicket operations. Cushy notes these operations, but it goes to the network on a regular timer driven basis and if an operation fails it retries after the next timer interval.
- For other mechanisms you configure a highly available database or multicast addresses, timeouts, and recovery parameters. Cushy uses HTTP, and you already have that from the Web server CAS is already running in. The only configuration is the URLs of the machines in the cluster.
- A lot of CAS users configure JPA just so they can reboot CAS without losing the tickets. Cushy does that without the database or cluster.
- Cushy is probably less efficient than other technologies, but if it uses less that 1% of one core of a modern server then, given the relative importance of CAS in most institutions, reducing that to a quarter of 1% is not worthwhile if you have to give something up to get the efficiency.
Basic Principles
- CAS is very important, but it is also small and cheap to run.
- Emphasize simplicity over efficiency as long as the cost remains trivial.
- The Front End gets the request first and it can be told what to do to keep the rest of the work simple. Let it do its job.
- Hardware failure doesn't have to be completely transparent. We can allow one or two users to get a bad message if everything works for the other 99.9% of the users. Trying to do better than this is the source of most 100% system failures.
Ticket Chains (and Test Cases)
A TGT represents a logged on user. It is called a Ticket Granting Ticket because it is used to create Service and Proxy tickets. It has no parent and stands alone.
When a user requests it, CAS uses the TGT to create a Service Ticket. The ST points to the TGT that created it, so when the application validates the ST id string, CAS can follow the chain from the ST to the TGT to get the Netid and attributes to return to the application. Then the ST is discarded.
However, when a middleware application like a Portal supports CAS Proxy protocol, the CAS Business Logic layer trades an ST (pointing to a TGT) in and turns it into a second type of TGT (the Proxy Granting Ticket or PGT). The term "PGT" exists only in documents like this. Internally CAS just creates a second TGT that points to the login TGT.
If the Proxy application accesses a backend application, it calls the /proxy service passing the TGT ID and gets back a Service Ticket ID. That ST points to the PGT that points to the TGT from which CAS can find the Netid.
So when you are thinking about Ticket Registries, or when you are designing JUnit test cases, there are four basic arrangements to consider:
- a TGT
- a ST pointing to a TGT
- a PGT pointing to a TGT
- a ST pointing to a PGT pointing to a TGT
This becomes an outline for various cluster node failure tests. Whenever one ticket points to a parent there is a model where the ticket pointed to was created on a node that failed and the new ticket has to be created on the backup server acting on behalf of that node. So you want to test the creation and validation of a Service Ticket on node B when the TGT was created on node A, or the creation of a PGT on node B when the TGT was created on node A, and so on.
Front End Programming
CAS Ticket IDs have four sections:
type - num - random - suffix
where type is "TGT" or "ST", num is a ticket sequence number, random is a large random string like "dmKAsulC6kggRBLyKgVnLcGfyDhNc5DdGKT", and the suffix at the end is configured in the XML.
There are separate XML configurations for different types of tickets, but they all look alike and they all occur in the uniqueIdGenerators.xml file. With cushy the suffix is tied to the TicketSuffix property generated by the CushyClusterConfiguration:
<bean id="ticketGrantingTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">
<constructor-arg index="0" type="int" value="50" />
<constructor-arg index="1" value="#{clusterConfiguration.getTicketSuffix()}" />
</bean>
So when Cushy figures out what cluster this computer is in and assigns each node a name, it generates the TicketSuffix value and feeds it to the ticket ID generation logic on each node. In the simplest case, the suffix is just the node name. The F5, however, likes to identity hosts by the MD5 hash of their IP address.
Every CAS request except the initial login comes with one or more tickets located in different places in the request. A modern programmable Front End device like the BIG-IP F5 can be programmed to understand the CAS protocol and to locate the important ticket. There is a sequence of tests and you stop at the first match:
- If the Path part of the URL is a validate request (/cas/validate, /cas/serviceValidate, /cas/proxyValidate, or /cas/samlValidate) then look at the ticket= parameter in the query string part of the URL
- If the Path part of the URL is a /cas/proxy request, then look at the pgt= parameter in the query string.
- If the request has a CASTGC cookie, then look at the cookie value.
- If a request has been seen from this browser in the last 5 minutes, then send it to the same node it was previously sent to.
- Otherwise, or if the node selected by 1-4 is down, choose any CAS node
That is the code, now here is the explanation:
- After receiving a Service Ticket ID, an application opens its own HTTPS session to CAS, presents the ticket id in a "validate" request. If the id is valid CAS passes back the Netid, and in certain requests can pass back additional attributes. The suffix on the ticket= parameter identifies the CAS server that created the ticket and has it in memory without requiring any high speed replication.
- When a middleware server like a Portal has obtained a CAS Proxy Granting Ticket, it requests CAS to issue a Service Ticket by making a /proxy call. Since the middleware is not a browser, it does not have a Cookie to hold the PGT. So it passes it explicitly in the pgt= parameter.
- After a user logs in, CAS creates a Login TGT that points to the Netid and attributes and writes the ticket id of the TGT to the browser as a Cookie. The Cookie is scoped to the URL of the CAS application as seen from the browser point of view. At Yale this is "https://secure.its.yale.edu/cas" and so whenever the browser sees a subsequent URL that begins with this string, it appends the CASTGC Cookie with the TGT ID. CAS uses this to find the TGT object and knows that the user has already logged in. This rule sends a browser back to the CAS node the user is logged into.
- If the first three tests fail, this request is not associated with an existing logged in user. CAS has a bug/feature that it depends on Spring Web Flow and stores data during login in Web Flow storage which in turn depends on the HTTPSession object maintained by the Web Server (Tomcat, JBoss, ...). You can cluster JBoss or Tomcat servers to share HTTPSession objects over the network, but it is simpler if you program the Front End so that if the user responds in a reasonable amount of time, the login form with the userid and password is send back to the Web Server that wrote the form it to the browser in response to the browser's original HTTP GET. This is called a "sticky session" and the F5 does it automatically if you just check a box. You don't need to write code.
- Otherwise, if this is a brand new request to login to CAS or if the CAS Server selected by one of the previous steps has failed and is not responding to the Front End, then send the request to any available CAS server.
What Cushy Does at Failure
It is not necessary to explain how Cushy runs normally. It is based on DefaultTicketRegistry. It stores the tickets in a table in memory. If you have a cluster, each node in the cluster operates as if it was a standalone server and depends on the Front End to route requests to the node that can handle them.
Separately from the CAS function, Cushy periodically writes some files to a directory on disk. They are ordinary files. They are protected with ordinary operating system security.
In a cluster, the files can be written to a shared disk, or they can be copied to a shared location or from node to node by an independent program that has access to the directories. Or, Cushy will replicate the files itself using HTTPS GET requests.
A failure is detected when a request is routed by the Front End to a node other than the node that created the ticket.
Because CAS is a relatively small application that can easily run on a single machine, a "cluster" can be configured in either of two ways:
- A Primary server gets all the requests until it fails. Then a Backup "warm spare" server gets requests. If the Primary comes back up relatively quickly, then Cushy will work best if Front End resumes routing all request to the Primary as soon as it becomes available again.
- Users are assigned to CAS Servers on a round-robin or load balanced basis.
Each CAS server in the cluster has a shadow object representing the TicketRegistry of each of the other nodes. In normal operation, that object contains no ticket objects. There is no need to read the files from the other node until a failure occurs and a request for one of those tickets arrives. Then Cushy restores the tickets from the file into memory (Just In Time) and processes requests on behalf of the failed node.
However, every new ticket Cushy creates belongs to the current node that created it. A new Service Ticket gets the suffix of the current node even if the Login TGT has the suffix of the failed node. A new Proxy Granting Ticket can also be created on this node for middleware even though the user logged into the different failed node.
This allows the Front End to do the right thing in the few seconds after the failed node reappears on the network. Requests that depend on the newly created tickets generated by the backup servers go back to the servers that created them. However, as soon as the login node reappears then new requests from the user's browser go back to the login server where new Service Tickets and PGTs are now created where we would prefer they be.
Service Tickets are created and then in a few milliseconds they are deleted when the application validates them or they time out after a few seconds or minutes. They do not exist long enough to raise any issues.
Proxy Granting Tickets, however, can remain around for hours. So the one long term consequence of a failure is that the login TGT can be on one server, but a PGT can be on a different server that created it while the login server was temporarily unavailable. This requires some thought, but you should quickly realize that everything will work correctly today. In future CAS releases there will be an issue if a user adds additional credentials (factors of authentication) to an existing login after a PGT is created. Without the failure, the PGT sees the new credentials immediately. With current Cushy logic, the PGT on the backup server is bound to a point in time snapshot of the original TGT and will not see the additional credentials. Remember, this only occurs after a CAS failure. It only affects the users who got the Proxy ticket during the failure. It can be "corrected" if the end user logs out and then logs back into the middleware server.
Cushy 2.0 will consider addressing this problem automatically.
There is also an issue with Single Sign Out. If a user logs out during a failure of his login server, then a backup server processes the Single Log Out normally. Then when the login server is restored to operation, the Login TGT is restored from the checkpoint file into memory. Of course, no browser now has a Cookie pointing to that ticket, so it sits unused all day and then in the evening it times out and a second Single Sign Out process is triggered and all the applications that perviously were told the user logged out are not contacted a second time with the same logout information. It is almost unimaginable that any application would be written so badly it would care about this, but it should be mentioned.
While the login server is down, new Service Tickets can be issued, but they cannot be meaningfully added to the "services" table in the TGT that drives Single Sign Out. After the login server is restored, if the user logs out to CAS the only applications that will be notified of the logout will be applications that received their Service Tickets from the logon server. Cushy regards Single Sign Out as a "best effort" service and cannot at this time guarantee processing for ST's issued during a node or network failure.
Again, Cushy 2.0 may address this problem.
Usage Pattern
Users start logging into CAS at the start of the business day. The number of TGTs begins to grow.
Users seldom log out of CAS, so TGTs typically time out instead of being explicitly deleted.
Users abandon a TGT when they close the browser. They then get a new TGT and cookie when they open a new browser window.
Therefore, the number of TGTs can be much larger than the number of real CAS users. It is a count of browser windows and not of people or machines.
At Yale around 3 PM a typical set of statistics is:
Unexpired-TGTs: 13821 Unexpired-STs: 12 Expired TGTs: 30 Expired STs: 11
So you see that a Ticket Registry is overwhelmingly a place to keep logon TGTs (in this statistic TGTs and PGTs are combined).
Over night the TGTs from earlier in the day time out and the Registry Cleaner deletes them.
So generally the pattern is a slow growth of TGTs while people are using the network application, followed by a slow reduction of tickets while they are asleep, with a minimum probably reached each morning before 8 AM.
If you display CAS statistics periodically during the day you will see a regular pattern and a typical maximum number of tickets in use "late in the day".
Translated to Cushy, the cost of the full checkpoint and the size of the checkpoint file grow over time along with the number of active tickets, and then the file shrinks over night. During any period of intense login activity the incremental file may be unusually large. If you had a long time between checkpoints, then around the daily minimum (8 AM) you could get an incremental file bigger than the checkpoint.
CAS Cluster
In this document a CAS "cluster" is just a bunch of CAS server instances that are configured to know about each other. The term "cluster" does not imply that the Web servers are clustered in the sense that they share Session objects (JBoss "clustering"). Nor does it depend on any other type of communication between machines. In fact, a CAS cluster could be created from a CAS running under Tomcat on Windows and one running under JBoss on Linux.
To the outside world, the cluster typically shares a common virtual URL simulated by the Front End device. At Yale, CAS is "https://secure.its.yale.edu/cas" to all the users and applications. The "secure.its.yale.edu" DNS name is associated with an IP address managed by the BIG-IP F5 device. It holds the certificate, terminates the SSL, then examines requests and based on programming called iRules it forwards requests to any of the configured CAS virtual machines.
Each virtual machine has a native DNS name and URL. It is these "native" URLs that define the cluster because each CAS VM has to use the native URL to talk to another CAS VM. At Yale those URLs follow a pattern of "https://vm-foodevapp-01.web.yale.internal:8443/cas".
Internally, Cushy configuration takes a list of URLs and generates a cluster definition with three pieces of data for each cluster member: a nodename like "vmfoodevapp01" (the first element of the DNS name with dashes removed), the URL, and the ticket suffix that identifies that node (the F5 prefers the ticket suffix to the an MD5 hash of the IP address of the VM).
Configuration
In CAS the TicketRegisty is configured using the WEB-INF/spring-configuration/ticketRegistry.xml file.
In the standard file, a bean with id="ticketRegistry" is configured selecting the class name of one of the optional TicketRegistry implementations (JBoss Cache, Ehcache, ...). To use Cushy you configure the CushyTicketRegistry class and its particular parameters.
Then at the end there are a group of bean definitions that set up periodic timer driven operations using the Spring support for the Quartz timer library. Normally these beans set up the RegistryCleaner to wake up periodically and remove all the expired tickets from the Registry.
Cushy adds a new bean at the beginning. This is an optional bean for class CushyClusterConfiguration that uses some static configuration information and runtime Java logic to find the IP addresses and hostname of the current computer to select a specific cluster configuration and generate property values that can be passed on to the CushyTicketRegistry bean. If this class does not do what you want, you can alter it, replace it, or just generate static configuration for the CushyTicketRegistry bean.
Then add a second timer driven operation to the end of the file to call the "timerDriven" method of the CushyTicketRegistry object on a regular basis (say once every 10 seconds) to trigger writing the checkpoint and incremental files.
The Cluster
We prefer a single "cas.war" artifact that works everywhere. It has to work on standalone or clustered environments, in a desktop sandbox with or without virtual machines, but also in official DEV (development), TEST, and PROD (production) servers.
There are techniques (Ant, Maven) to "filter" a WAR file replacing one string of text with another as it is deployed to a particular host. While that works for individual parameters like "nodeName", the techniques that are available make it hard to substitute a variable number of elements, and some locations have one CAS node in development, two CAS nodes in test, and three CAS nodes in production.
Then when we went to Production Services to actually deploy the code, they said that they did not want to edit configuration files. They wanted a system where the same WAR is deployed anywhere and when it starts up it looks at the machine it is on, decides that this a TEST machine (because it has "tst" in the hostname), and so it automatically generates the configuration of the TEST cluster.
At this point you should have figured out that it would be magical if anyone could write a class that reads your mind and figures out what type of cluster you want. However, it did seem reasonable to write a class that could handle most configurations out of the box and was small enough and simple enough that you could add any custom logic yourself.
The class is CushyClusterConfiguration and it is separate from CushyTicketRegistry to isolate its entirely optional convenience features and make it possible to jiggle the configuration logic without touching the actual TicketRegistry. It has two configuration strategies:
First, you can configure a sequence of clusters (desktop sandbox, and machine room development, test, and production) by providing for each cluster a list of the machine specific raw URL to get to CAS (from other machines also behind the machine room firewall). CusyClusterConfiguration look up all the IP addresses of the current machine, then looks up the addresses associated with the servers in each URL in each cluster. It chooses the first cluster that it is in (that contains a URL that resolves to an address of the current machine).
Second, if none of the configured clusters contains the current machine, or if no configuration is provided, then Cushy uses the HOSTNAME and some Java code to automatically configure the cluster. At this point we expect you to provide some programming, unless you can use the Yale solution off the shelf.
At Yale we know that CAS is a relatively small application with limited requirements, and that any modern multi-core server can certainly handle all the CAS activity of the university (or even of a much larger university). So we always create clusters with only two nodes, and the other node is just for recovery from a serious failure (and ideally the other node is in another machine room far enough away to be outside the blast radius).
In any given cluster, the hostname of both machines is identical except for a suffix that is either the three characters "-01" or "-02". So by finding the current HOSTNAME it can say that if this machine has "-01" in its name, the other machine in the cluster is "-02", or the reverse.
Configuration By File
You can define the CushyClusterConfiguration bean with or without a "clusterDefinition" property. If you provide the property, it is a List of Lists of Strings:
<bean id="clusterConfiguration" class="edu.yale.its.tp.cas.util.CushyClusterConfiguration"
p:md5Suffix="yes" >
<property name="clusterDefinition">
<list>
<!-- Desktop Sandbox cluster -->
<list>
<value>http://foo.yu.yale.edu:8080/cas/</value>
<value>http://bar.yu.yale.edu:8080/cas/</value>
</list>
<!-- Development cluster -->
<list>
<value>https://casdev1.yale.edu:8443/cas/</value>
<value>https://casdev2.yale.edu:8443/cas/</value>
</list>
...
</list>
</property>
</bean>
In spring, the <value> tag generates a String, so this is what Java calls a List<List<String>> (List of Lists of Strings). As noted, the top List has two elements. The first element is a List with two Strings for the machines foo and bar. The second element is another List with two strings for casdev1 and casdev2.
There is no good way to determine all the DNS names that may resolve to an address on this server. However, it is relatively easy in Java to find all the IP addresses of all the LAN interfaces on the current machine. This list may be longer than you think. Each LAN adapter can have IPv4 and IPv6 addresses, and then there can be multiple real LANs and a bunch of virtual LAN adapters for VMWare or Virtualbox VMs you host or tunnels to VPN connections. Of course, there is always the loopback address.
So CushyClusterConfiguration goes to the first cluster (foo and bar). It does a name lookup (in DNS and in the local etc/hosts file) for each server name (foo.yu.yale.edu and bar.yu.yale.edu). Each lookup returns a list of IP addresses associated with that name.
CushyClusterConfiguration selects the first cluster and first host computer whose name resolves to an IP address that is also an address on one of the interfaces of the current computer. The DNS lookup of foo.yu.yale.edu returns a bunch of IP addresses. If any of those addresses is also an address assigned to any real or virtual LAN on the current machine, then that is the cluster host name and that is the cluster to use. If not, then try again in the next cluster.
CushyClusterConfiguration can determine if it is running in the sandbox on the desktop, or if it is running the development, test, production, disaster recovery, or any other cluster definition. The only requirement is that IP addresses be distinct across servers and cluster.
Restrictions (if you use a single WAR file with a single global configuration):
It is not generally possible to determine the port numbers that a J2EE Web Server is using. So it is not possible to make distinctions based only on port number. CushyClusterConfiguration requires a difference in IP addresses. So if you want to emulate a cluster on a single machine, use VirtualBox to create VMs and don't think you can run two Tomcats on different ports.
(This does not apply to Unit Testing, because Unit Testing does not use a regular WAR and is not constrained to a single configuration file. If you look at the unit tests you can see examples where there are two instances of CushyTicketRegistry configured with two instances of CushyClusterConfiguration with two cluster configuration files. In fact, it can be a useful trick that the code stops at the first match. If you edit the etc/hosts file to create a bunch of dummy hostnames all mapped on this computer to the loopback address (127.0.0.1), then those names will always match the current computer and Cushy will stop when it encounters the first such name. The trick then is to create for the two test instances of Cushy two configuration files (localhost1,localhost2 and localhost2,localhost1). Fed the first configuration, that test instance of Cushy will match the first name (localhost1) and will expect the cluster to also have the other name (localhost2). Fed the second configuration the other test class will stop at localhost2 (which is first in that file) and then assume the cluster also contains localhost1.)
Any automatic configuration mechanism can get screwed up by mistakes made by system administrators. In this case, it is a little easier to mess things up in Windows. You may have already noticed this if your Windows machine hosts VMs or if your home computer is a member of your Active Directory at work (though VPNs for example). At least you would see it if you do "nslookup" to see what DNS thinks of your machine. Windows has Dynamic DNS support and it is enabled by default on each new LAN adapter. After a virtual LAN adapter has been configured you can go to its adapter configuration, select IPv4, click Advanced, select the DNS tab, and turn off the checkbox labelled "Register this connection's addresses in DNS". If you don't do this (and how many people even think to do this), then the private IP address assigned to your computer on the virtual LAN (or the home network address assigned to your computer when it has a VPN tunnel to work) gets registered to the AD DNS server. When you look up your machine in DNS you get the IP address you expected, and then an additional address of the form 192.168.1.? which is either the address of your machine on your home LAN or its address on the private virtual LAN that connects it to VMs it hosts.
Generally the extra address doesn't matter. A problem only arises when another computer that is also on a home or virtual network with its own 192.168.1.* addresses looks up the DNS name of a computer, gets back a list of addresses, and for whatever reason decides that that other computer is also on its home or virtual LAN instead of using the real public address that can actually get to the machine.
CushyClusterConfiguration is going to notice all the addresses on the machine and all the addresses registered to DNS, and it may misidentify the cluster if these spurious internal private addresses are being used on more than one sandbox or machine room CAS computer. It is a design objective of continuing Cushy development to refine this configuration process so you cannot get messed up when a USB device you plug into your computer generates a USB LAN with a 192.168.153.4 address for your computer, but to do this in a way that preserves your ability to configure a couple of VM guests on your desktop for CAS testing.
Note also that the Unit Test cases sometimes exploit this by defining dummy hostnames that resolve to the loopback address and therefore are immediately matched on any computer.
In practice you will have a sandbox you created and some machine room VMs that were professionally configured and do not have strange or unexpected IP addresses, and you can configure all the hostnames in a configuration file and Cushy will select the right cluster and configure itself the way you expect.
Autoconfigure
At Yale the names of DEV, TEST, and PROD machines follow a predictable pattern, and CAS clusters have only two machines. So production services asked that CAS automatically configure itself based on those conventions. If you have similar conventions and any Java coding expertise you can modify the autoconfiguration logic at the end of CushyClusterConfiguration Java source.
CAS is a relatively simple program with low resource utilization that can run on very large servers. There is no need to spread the load across multiple servers, so the only reason for clustering is error recovery. At Yale a single additional machine is regarded as providing enough recovery.
At Yale, the two servers in any cluster have DNS names that ends in "-01" or "-02". Therefore, Cushy autoconfigure gets the HOSTNAME of the current machine, looks for a "-01" or "-02" in the name, and when it matches creates a cluster with the current machine and one additional machine with the same name but substituting "-01" for "-02" or the reverse.
Standalone
If no configured cluster matches the current machine IP addresses and the machine does not autoconfigure (because the HOSTNAME does not have "-01" or "-02"), then Cushy configures a single standalone server with no cluster.
Even without a cluster, Cushy still checkpoints the ticket cache to disk and restores the tickets across a reboot. So it provides a useful function in a single machine configuration that is otherwise only available with JPA and a database.
You Can Configure Manually
Although CushyClusterConfiguration makes most configuration problems simple and automatic, if it does the wrong thing and you don't want to change the code you can ignore it entirely. As will be shown in the next section, there are three properties, a string and two Properties tables) that are input to the CusyTicketRegistry bean. The whole purpose of CushyClusterConfiguration is to generate a value for these three parameters. If you don't like it, you can use Spring to generate static values for these parameters and you don't even have to use the clusterConfiguration bean.
Other Parameters
Typically in the ticketRegistry.xml Spring configuration file you configure CushyClusterConfiguration as a bean with id="clusterConfiguration" first, and then configure the usual id="ticketRegistry" using CusyTicketRegistry. The clusterConfiguration bean exports some properties that are used (through Spring EL) to configure the Registry bean.
<bean id="ticketRegistry" class="edu.yale.cas.ticket.registry.CushyTicketRegistry"
p:serviceTicketIdGenerator-ref="serviceTicketUniqueIdGenerator"
p:checkpointInterval="300"
p:cacheDirectory= "#{systemProperties['jboss.server.data.dir']}/cas"
p:nodeName= "#{clusterConfiguration.getNodeName()}"
p:nodeNameToUrl= "#{clusterConfiguration.getNodeNameToUrl()}"
p:suffixToNodeName="#{clusterConfiguration.getSuffixToNodeName()}" />
The nodeName, nodeNameToUrl, and suffixToNodeName parameters link back to properties generated as a result of the logic in the CushyClusterConfiguration bean.
The cacheDirectory is a work directory on disk to which it has read/write privileges. The default is "/var/cache/cas" which is Unix syntax but can be created as a directory structure on Windows. In this example we use the Java system property for the JBoss /data subdirectory when running CAS on JBoss.
The checkpointInterval is the time in seconds between successive full checkpoints. Between checkpoints, incremental files will be generated.
CushyClusterConfiguration exposes a md5Suffix="yes" parameter which causes it to generate a ticketSuffix that is the MD5 hash of the computer host instead of using the nodename as a suffix. The F5 likes to refer to computers by their MD5 hash and using that as the ticket suffix simplifies the F5 configuration even though it makes the ticket longer.
There are other "properties" that actually turn code options on or off. Internally they are static variable that only appear to be properties of the CushyTicketRegistry class so they can be added to the ticketRegistry.xml file. The alternative would be to make them static values in the source and require you to recompile the source to make a change.
- p:sharedDisk="true" - disables HTTP communication for JUnit Tests and when the work directory is on a shared disk.
- p:disableJITDeserialization="true" - disables an optimization that only reads tickets from a checkpoint or incremental file the first time the tickets are actually needed. The only reason for using this parameter is during testing so that the number of tickets read from the file appears in the log immediately after the file is generated.
- p:excludeSTFromFiles="true" - this is plausibly an option you should use. It prevents Service Tickets from being written to the checkpoint or incremental files. This makes incremental files smaller because it is then not necessary to keep the growing list of ST IDs for all the Service Tickets that were deleted probably before anyone ever really cared about them.
- p:useThread="true" - use a thread to read the checkpoint file from another CAS node. If not set, the file is read in line and this may slow down the processing of a new checkpoint across all the nodes.
How Often?
"Quartz" is the standard Java library for timer driven events. There are various ways to use Quartz, including annotations in modern containers, but JASIG CAS uses a Spring Bean interface to Quartz where parameters are specified in XML. All the standard JASIG TicketRegistry configurations have contained a Spring Bean configuration that drives the RegistryCleaner to run and delete expired tickets every so often. CushyTicketRegistry requires a second Quartz timer configured in the same file to call a method that replicates tickets. The interval configured in the Quartz part of the XML sets a base timer that determines the frequency of the incremental updates (typically every 5-15 seconds). A second parameter to the CushyTicketRegistry class sets a much longer period between full checkpoints of all the tickets in the registry (typically every 5-10 minutes).
A full checkpoint contains all the tickets. If the cache contains 20,000 tickets, it takes about a second to checkpoint, generates a 3.2 megabyte file, and then has to be copied across the network to the other nodes. An incremental file contains only the tickets that were added or deleted since the last full checkpoint. It typically takes a tenth of a second an uses very little disk space or network. However, after a number of incrementals it is a good idea to do a fresh checkpoint just to clean things up. You set the parameters to optimize your CAS environment, although either operation has so little overhead that it should not be a big deal.
Based on the usage pattern, at 8:00 AM the ticket registry is mostly empty and full checkpoints take no time. Late in the afternoon the registry reaches its maximum size and the difference between incrementals and full checkpoints is at its greatest.
Although CAS uses the term "incremental", the actual algorithm is a differential between the current cache and the last full checkpoint. So between full checkpoints, the incremental file size increases as it accumulates all the changes. Since this also includes a list of all the Service Ticket IDs that were deleted (just to be absolutely sure things are correct), if you made the period between full checkpoints unusually long it is possible for the incremental file to become larger than the checkpoint and since it is transferred so frequently this would be much, much worse to performance than setting the period for full checkpoints to be a reasonable number.
Nodes notify each other of a full checkpoint. Incrementals occur so frequently that it would be inefficient to send messages around. A node picks up the other incrementals from the other nodes each time it generates its own incremental.
Special Situations
Cushy stores tickets in an in-memory table. It writes tickets to a disk file with a single writeObject Java statement. It transfers files from machine to machine using an HTTPS GET. So far, everything seems to be rather simple. Cushy started that way, but then it became clear that there were a small number of optimizations that really needed to be made even if they added a slight amount of complexity to the code.
Notify
Once every 5-15 minutes a node generates a new full checkpoint file. It also generates a new dummy ServiceTicketId that acts as the password that other nodes will present to request the files over HTTPS. It then does a "Notify" operation. It generates a HTTPS GET to the /cas/cluster/notify URL on every other CAS node in the cluster. This request is routed by Spring MVC to the CacheNotifyController class provided by the Cushy package. A node also does a Notify immediately after it reboots to inform the other nodes that it is back up and to provide them with the password needed to communicate until the next checkpoint.
The Notify goes to every node in the cluster at its configured URL. The URL is assumed to be "https:" so the SSL Certificate in the other node verifies that it is the correct machine authorized to receive the data.
However, when a node receives what looks like a Notify it cannot verify its source. This is not a big problem because the first order of business is to read the new checkpoint file from the node sending the Notify, and to read the file it uses the configured URL for that node in the cluster definition, and since that URL is "https:" it will only work if the other node has a Certificate proving its identity, and if the other node accepts the secret dummy ServiceTicketId sent in the Notify then the loop has been closed. Both machines communicated over configured URLs. Both verified their identity with a Certificate. All data was encrypted with SSL. The ticket send on the Notify was validated when the checkpoint file was returned correctly.
Because Notify is sent when CAS boots up, it is an indication that the node is "healthy" that resets any flag indicating that the node is "sick". This does not, however, prevent the other nodes from reacting if they continue to receive requests or exceptions indicating a problem. When Cushy gets an indication of a problem it sets a flag. It then continues of operate assuming the problem is still there until it gets a Notify from the node. After the Notify, Cushy does not assume that there is a continuing problem, but it will respond appropriately if one is detected.
Just In Time Deserialization
In the first cut, each checkpoint and incremental file was turned into objects immediately after it was read. Then it became clear that Cushy was using all this processing time to create objects that just got discarded a few minutes later without anyone actually using them. The objects are in the file, but the file does not need to be "deserialized" to create the objects until someone calls for a ticket, and that only happens after a node failure.
So the 99.9% of the time when there is no CAS node failure, the files may be transferred but no objects are created, which saves both the time of deserialization and the subsequent Garbage Collection cycle discarding all the obsolete ticket objects. Once a request arrives and a ticket is requested from another node's registry, the objects are created and then maintained by applying incrementals.
When a Notify is received from the node in question, the checkpoint file is read from that node but it is not deserialized. Instead, Cushy tentatively goes back to its Just In Time state on the reasonable chance that the Front End may resume sending requests to the node and the ticket objects won't be required. That may not be true after the first Notify, but eventually a Notify will clear the state, no more requests will arrive for tickets from the other node, and Cushy will go back to only copying files.
Generally speaking, while the other node is up Cushy copies files but does not create objects. Generally speaking, when the other node fails Cushy creates all the objects once, and then while the other node is down there are no new files being generated and so the objects deserialized once are used to process requests for that node. Generally speaking, when the node comes back up it sends a Notify and everything goes back to normal. So generally Cushy is not creating a lot of objects for other nodes. However, the view of network health from the point of view of the CAS nodes is different from the view from the Front End. The Front End may not be able to get to a node that is up and can still speak to all the other nodes. It stops getting requests, but every 5-15 minutes it generates a new checkpoint (perhaps with only changes from the RegistryCleaner) and it sends a Notify to all the other nodes. In this type of network failure the other nodes are discarding all the tickets for the "failed" node and reloading new copies of the tickets every 5-15 minutes. This may put a bit of work on the Garbage Collector, but nothing so bad it should cause trouble.
SharedDisk
The SharedDisk parameter is typically specified in the ticketRegistry.xml Spring configuration file. It turns off the Cushy HTTP processing. There will be no Notify message, and therefore no HTTP fetching of the checkpoint or incremental file. There is no exchange of dummy ServiceTicketId for communication security because there is no communication. It is used in real SharedDisk situations and in Unit Test cases.
Since there is no notify, the timerDriven code that generates checkpoint and incremental files has to check the last modified timestamp on the checkpoint file of any other node. If the timestamp changes, then that triggers the subset of Notify processing that does not involve HTTP or file transfers (like the resetting of flags indicating possible node health).
Cold Start Quiet Period
When CAS starts up and finds no previous checkpoint file in its work directory, there are no tickets to restore. This is a Cold Start, and it may be associated with a change of CAS code from one release to another with possible changes to the Ticket object definitions. A cold start has to happen at one time and it has to restart all the servers the cluster. You do not want one server running on old code while another server runs on the new code. To give the operators time to make the change, after a cold start CAS enters the Cold Start Quiet Period which lasts for 10 minutes (built into the source). During this period it does not send or respond to HTTP requests from other nodes. That way the nodes cannot exchange mismatched object files.
Healthy
When CAS receives an HTTP GET I/O error attempting to contact or read data from another node, it marks that node as "unhealthy" It then waits for a Notify from the node, and then tries to read the new checkpoint file.
Without the "healthy" flag, when a node goes down all the other nodes would attempt every 10 seconds or so to read a new incremental file but the HTTP connect would time out. Adding a timeout every 10 seconds seems like a waste, and the Notify process will tell us soon enough when it is time to reconsider the health of the node.
Note that Healthy deals with a failure of this server to connect to a node while Just In Time Deserialization is triggered when the Front End cannot get to the node and sends us a request that belongs to the other node. If a node really goes down, both things happen at roughly the same time. Otherwise, it is possible for just one type of communication to fail while the other still works.
The CAS Problems Cushy Can't Fix on its Own
Serialization isn't Thread Safe unless You Make It
A Web server handles lots of different HTTP requests from clients at the same time. It assigns a thread to each request. The threads run concurrently, and on modern multicore processors they can run simultaneously.
If an object has a collection (a table or list of objects) that can be updated by these requests, then it has to take some step to make sure that no two requests try to update the collection at the same time. The TGT has a collection of Services to which the user has authenticated (for Single Sign Out) and in CAS 4 it also has a List of Supplemental Authentications. CAS 3 was sloppy about this, but CAS 4 adds "synchronized" methods to protect against concurrent access to these tables by different Web request threads.
Unfortunately, serialization accesses the object and its internal collections without going through any of the synchronized methods. It has to iterate through all the members of the table or the list, and in general it cannot do this in a thread safe manner. Because serialization occurs when some external component (Ehcache, JBoss Cache, ...) decides to do it, and that decision is made deep inside what amounts to a giant black box of code, there is no way to externally guarantee that something won't go wrong.
One solution (that CAS has not implemented yet) is to create a custom serialization method of the Ticket objects that is synchronized between threads. The code is standard and simple:
private synchronized void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
This "solution" is not without controversy. It should work correctly for CAS using any of the TicketRegistry alternatives, but it cannot be guaranteed to work when you decide to use a large "black box" of complex logic.The problem it creates is a threat of Deadlock.
Deadlock occurs when I own object A and need to acquire ownership of object B, while you own object B and request ownership of object A. Neither of us can get what we want, and neither of us will give up the thing the other wants. Any synchronized mechanism is exposed to deadlock unless you can enforce rules on your code to make sure it never happens.
The simplest solution is to prohibit any code from obtaining exclusive ownership of more than one object at a time. If that doesn't work, then the objects have to be obtained in a specific order by universal agreement.
CAS only acquires ownership of one object at a time. Serialization would only acquire objects one at at time. Cushy only acquires ownership of one object at a time. However, who knows what Ehcache, JBoss Cache, Memcached, or other systems do? It is regarded as very bad practice to do disk or network I/O or to use complex services like serialization while holding exclusive ownership of an object. These systems are probably safe, but I lack the resources to prove they are safe.
Deserialized Objects get a Private Copy of the TGT
However, current (CAS 3 and CAS 4) code creates a different problem of its own, and this is an issue no matter what TicketRegistry you use. The TGT is not an entirely static collection of objects. In CAS 3 there is a table of ST IDs and Service URLS used by Single Log Off and new entries are added to the table every time a Service Ticket is created. In CAS 4 there is an array of supplimentalAuthentications.
When you serialize a ST or PGT individually then the stream of bytes generated by writeObject includes all the objects that it points to, include the TGT and all it's stuff. When this gets deserialized at the other end, a copy of all these objects is created. So you cannot really serialize a ST or PGT by itself.
If you serialize the entire registry of tickets, as Cushy does during a full checkpoint, then when you deserialize it you get an exact copy with all the same connections and structure. However, if you serialize an individual ticket, as Cushy does during an incremental and as all the "cache" based object replication systems do for everything, then each ST or PGT gets its own private copy of the original TGT frozen at the time it was serialized.
This is absolutely not a problem now, because CAS 3 and CAS 4.0 TGTs don't meaningfully change after they are created. It is not plausibly a problem for Service Tickets because they don't live long. However, when you start to exploit multifactor authentication and use the supplimentalAuthentications table then changes you make to the TGT after you create a PGT will have different behavior on different nodes. On the node that created both the TGT and PGT then changes to the TGT become visible to the Proxy and to services it tries to access. On any other node, the PGT has its own private copy of the TGT frozen when the PGT was created and changes to the real TGT are not visible.
Cushy automatically solves this problem every time it takes a full checkpoint. The other nodes obtain a fresh exact copy of all the tickets on the other node connected together exactly as they are on the other node with the very latest information.
For Now
Current CAS simply ignores these issues and it doesn't seem to have any problems doing so. Every so often you may get an exception in the log during serialization caused by threading problems.
Otherwise, you have to change the Ticket classes in cas-server-core.
Yale does not use Single Sign Out, so we do not need the "Services" table in the TGT. We disable updates to the table and without the table the CAS 3 TGT is thread safe enough to be reliable.
If we used Single Sign Out and Cushy, then we would modify the Ticket objects to add the synchronized writeObject. You can do this with Cushy because you can verify from the code that a deadlock is impossible. You could cross your fingers with the other Registry solutions.
JUnit Testing
It is unusual for JUnit test cases to get their own documentation. Testing a cluster on a single machine without a Web server is complicated enough that the strategies require some documentation.
If you create an instance of CushyTicketRegistry without any parameters, it believes that it is a Primary object. You can then set properties and simulate Spring configuration. There is an alternate constructor with four parameters that is used only from test cases.
The trick here is to create two Primary CusyTicketRegistry instances with two compatible but opposite configurations. Typically one Primary object believes that it is node "casvm01" and that the cluster consists of a second node named "casvm02", while the other Primary object believes that it is node "casvm02" in a cluster with "casvm01".
The next thing you need is to make sure that both objects are using the same work directory. That way the first object will create a checkpoint file named "casvm01" and the other will create a checkpoint file named "casvm02".
Without a Web server, the files cannot be exchanged over the network. You cannot unit test the HTTP part. For the rest, once both nodes have checkpointed their tickets to the same directory, each node can then be programmed to skip over the HTTPS GET and just restore the file named for the other node from disk to its Secondary object for that node. Neither Primary object knows that the file for the other node was written directly to disk from another object in the same JVM rather than being fetched over the network.
There are two test classes with entirely different strategies.
CushyTicketRegistryTest.java tests the TicketRegistry interface and the Cushy functions of checkpoint, restore, writeIncremental, and readIncremental. You can create a single ticket or a 100,000 TGTs. This verifies that the tickets are handled correctly, but it does not test CAS Business Layer processing.
This test case creates a new empty TicketRegistry for each test, so it is good for checking that a sequence of operations produces an expected outcome.
CushyCentralAuthenticationServiceImplTests.java is an adaptation of the CentralAuthenticationServiceImpl test class from cas-server-core that simulates CAS Business Logic on two nodes across a failover. As with the original code, it uses Spring support for JUnit testing. It has a single resource file named applicationContext.xml that configures a stripped down CAS using versions of the same XML used to configure real CAS. In this case, however, there are two "ticketRegistry" beans that use two "clusterConfiguration" beans for nodes "casvm01" and "casvm02".
Warning: To make this test case work you need a line in your /etc/hosts or your c:\Windows\system32\drivers\etc\hosts" file that maps the names "casvm01" and "casvm02" to the loopback address, as in:
127.0.0.1 casvm01,casvm02
Without this the CushyClusterConfiguration beans cannot be tricked into regarding the one machine as if it was two nodes.
Using this test class, the Spring configuration is done first and then each test is run. As a result the two CushyTicketRegistry objects are not reinitialized between tests and the objects created in previous tests are left behind at the start of the next test. However, because the operations here involve the Business Logic layer, you can perform tests like:
Create credentials on casvm02
Create a TGT with the credentials on casvm02
Simulate a failure of casvm02, from now on everything is casvm01
Create a ST using the TGT ID of the casvm02 TGT.
Use the ST to create a PGT.
Create a ST using the PGT.
Validate the ST. Make sure that the netid that comes back matches the credentials supplied to casvm02.