Drupal Address Field Module Doesn't Scale
Address Field is a handy Drupal module--spun out of Commerce--that provides a field able to be attached to any Drupal entity. It stores addresses in a standard format and provides a reasonable level of integration with the rest of Drupal (for example, with Views or the Entity API). Though most useful for providing billing or shipping during Commerce checkout, it's also more than adequate for any situation where an address needs to be entered or displayed (e.g. a RedHen contact or a user profile).
One of the more attractive features of Address Field is its ability to dynamically swap, re-order, and re-label its component fields based on a country's norms. For instance, while in the United States, postal codes are known as a "zip codes" and there is a defined list of 50+ states, the United Kingdom refers to them as "postcodes" and calls its administrative divisions "counties." Along the same lines, Brazil tends to write its addresses with a postal code prior to its administrative divisions. Address Field is aware of many of these differences and handles them gracefully.
Though this feature provides a nice user experience for international site visitors, some peculiarities in Address Field, as well as in the way core Drupal builds and processes forms, prevent this feature from scaling well when dealing with high volumes of unauthenticated traffic.
Form and page cache TTLs
The simplest of the issues is a bug in Core wherein site administrators are allowed to configure Drupal's page cache entries to outlive its form cache entries. While page cache minimum lifetime and page cache maximum age are configurable up to 24 hours, form cache entries are hard-coded to live only 6 hours. On simpler forms, this is less of an issue, but this can cause problems on AJAX-enabled form fields like Address Field.
The AJAX callback provided by Drupal's form API must be able to pull a given form's cache entry, keyed by a unique form build ID. This build ID is generated on the initial rendering of the form's host page. Though the build ID can continue to live on, embedded in its host page's cache entry, AJAX functionality will cease to function because its associated entry in the form cache bin no longer exists.
There's an issue to fix this in Drupal 8, but a simple workaround for everyone now is to keep your page cache min and max TTLs at or below 6 hours.
Unauthenticated users and form build IDs
I mentioned that form build IDs are generated on initial page render and continue to live as long as the associated page lives. For authenticated users, that means a fresh build ID will be generated each time the page is refreshed.
For unauthenticated users, that means the same build ID will be used despite subsequent refreshes. Moreover, two completely distinct sessions can and will make use of the same build ID (and therefore, the same form cache entry).
This in and of itself is not necessarily a bug, but it can cause major headaches in high traffic situations where Drupal may be unknowingly validating one session's form submissions against another.
Country selection and form rebuilds
The Drupal Form API offers rudimentary AJAX functionality, allowing forms to be validated and rebuilt based on triggering elements. Address Field offers the dynamic functionality described above by listening for form submissions triggered by a user changing his country selection.
In those situations, it compares the input country selected by the user against the value it currently has stored as its default in the form cache. If a change is detected, it sets the country's value to that selected by the user and informs Drupal that, rather than validating and submitting the form, it should rebuild the form and deliver the rebuilt markup. Drupal obliges and new fields, labels, and field order for the address are delivered and applied asynchronously. The selected country then becomes the default country, stashed away in the form cache.
The user may either continue filling out and submitting the form or abandon the form. In either case, the user notices nothing unusual.
The next user to visit the same page will encounter some oddities. Suppose he or she fills out the form and submits it without changing the country. In this case, Drupal begins processing the form until Address Field runs its country validation check and notices that the country submitted by the user does not match the default value it has stored in the form cache. Though this user didn't change the country, because a previous user did, Address Field marks the form for rebuild, aborting the form submission and spitting the user back out on the previous page with no indication of error or success.
There is an issue open against Address Field to fix this, but it hasn't been reviewed in some time. The workaround is to trigger a cache clear of the host page as soon as an unauthenticated user triggers a change in country. With this in place, any subsequent visits to the page will trigger a full page rebuild, and thus a new form build ID and associated cache entry.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * An element validation handler for address field countries (added to validation processing elsewhere).<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">my_module_country_element_validation_handler</span><span style="color: #007700">(</span><span style="color: #0000BB">$element</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">, </span><span style="color: #0000BB">$form</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">_form_element_triggered_scripted_submission</span><span style="color: #007700">(</span><span style="color: #0000BB">$element</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">drupal_get_messages</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">drupal_static_reset</span><span style="color: #007700">(</span><span style="color: #DD0000">'form_set_error'</span><span style="color: #007700">);<br> if (isset(</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">'HTTP_REFERER'</span><span style="color: #007700">]) && !</span><span style="color: #0000BB">user_is_logged_in</span><span style="color: #007700">()) {<br> </span><span style="color: #0000BB">cache_clear_all</span><span style="color: #007700">(</span><span style="color: #0000BB">$_SERVER</span><span style="color: #007700">[</span><span style="color: #DD0000">'HTTP_REFERER'</span><span style="color: #007700">], </span><span style="color: #DD0000">'cache_page'</span><span style="color: #007700">);<br> }<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Form validation race conditions
Though the workarounds described above make for a greatly improved user experience in most cases, there are still edge cases in which users can be frustrated.
Notably, the "host-page cache clear" technique will be ineffective in the event two users load a page at about the same time. If one user triggers a country change, the other user is still stuck with an orphaned build ID. Though it may sound unlikely, when dealing with thousands of form submissions a day across hundreds of pages, even 20 or 30 occurrences a day are still possible.
The workaround for this issue requires that we highlight another.
Administrative division validation errors
One drawback to a dynamic administrative division field is that Drupal Core's validation handlers can become very easily confused. On select lists, Drupal will validate the user's input against the known list of options used to build the list in the first place. If the user's input does not exist in the known list, it throws an error with the message, "An illegal choice was detected, contact the site administrator."
In the race condition situation described above, if the country selected also happens to have a known list of administrative divisions (e.g. from the US to Canada), the second user on the orphaned form build ID will almost certainly get the error because his choice of a US state will be validated against a list Canadian provinces.
The only workaround is to detect the "illegal choice" validation error against the administrative area form element in particular, suppress it, and trick Drupal into continuing to process and submit the form.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * An element validation handler for Address Field administrative divisions (added to validation processing elsewhere).<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">my_module_state_element_validation_handler</span><span style="color: #007700">(</span><span style="color: #0000BB">$element</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">, </span><span style="color: #0000BB">$form</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$state_validation_avoided </span><span style="color: #007700">= &</span><span style="color: #0000BB">drupal_static</span><span style="color: #007700">(</span><span style="color: #0000BB">__FUNCTION__</span><span style="color: #007700">, </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$message </span><span style="color: #007700">= </span><span style="color: #DD0000">'An illegal choice has been detected. Please contact the site administrator.'</span><span style="color: #007700">;<br> if (</span><span style="color: #0000BB">$error </span><span style="color: #007700">= </span><span style="color: #0000BB">form_get_error</span><span style="color: #007700">(</span><span style="color: #0000BB">$element</span><span style="color: #007700">) AND </span><span style="color: #0000BB">$error </span><span style="color: #007700">== </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #0000BB">$message</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$key </span><span style="color: #007700">= </span><span style="color: #0000BB">implode</span><span style="color: #007700">(</span><span style="color: #DD0000">']['</span><span style="color: #007700">, </span><span style="color: #0000BB">$element</span><span style="color: #007700">[</span><span style="color: #DD0000">'#parents'</span><span style="color: #007700">]);<br> </span><span style="color: #0000BB">$errors </span><span style="color: #007700">= &</span><span style="color: #0000BB">drupal_static</span><span style="color: #007700">(</span><span style="color: #DD0000">'form_set_error'</span><span style="color: #007700">);<br> unset(</span><span style="color: #0000BB">$errors</span><span style="color: #007700">[</span><span style="color: #0000BB">$key</span><span style="color: #007700">]);<br> </span><span style="color: #0000BB">_function_to_remove_drupal_message</span><span style="color: #007700">(</span><span style="color: #0000BB">$message</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$state_validation_avoided </span><span style="color: #007700">= </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #FF8000">/**<br> * Another element validation handler for Address Field countries.<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">my_module_country_element_validation_handler2</span><span style="color: #007700">(</span><span style="color: #0000BB">$element</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">, </span><span style="color: #0000BB">$form</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">drupal_static</span><span style="color: #007700">(</span><span style="color: #DD0000">'scale_addressfield_state_validation_errors'</span><span style="color: #007700">)) {<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'rebuild'</span><span style="color: #007700">] = </span><span style="color: #0000BB">FALSE</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Download Scale Address Field
Though there are legitimate bugs to be tackled in the issues I've mentioned previously, you need a solution to the problem quicker than Address Field or Drupal's development cycle.
To address the gap, I've created a small utility module called Scale Address Field that fixes or mitigates all of the underlying issues described above. I'm releasing it via GitHub, rather than drupal.org, in hopes that Address Field and Core are able to quickly patch up the deficiencies (thus alleviating the need for the module altogether).
You can download Scale Address Field here, or do the same with drush:
drush dl scale_addressfield --source=http://www.asmallwebfirm.net/drupal/release-history