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.

<?php
/**
* An element validation handler for address field countries (added to validation processing elsewhere).
*/
function my_module_country_element_validation_handler($element, $form_state, $form) {
  if (
_form_element_triggered_scripted_submission($element, $form_state)) {
   
drupal_get_messages();
   
drupal_static_reset('form_set_error');
    if (isset(
$_SERVER['HTTP_REFERER']) && !user_is_logged_in()) {
     
cache_clear_all($_SERVER['HTTP_REFERER'], 'cache_page');
    }
  }
}
?>

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.

<?php
/**
* An element validation handler for Address Field administrative divisions (added to validation processing elsewhere).
*/
function my_module_state_element_validation_handler($element, $form_state, $form) {
 
$state_validation_avoided = &drupal_static(__FUNCTION__, FALSE);
 
$message = 'An illegal choice has been detected. Please contact the site administrator.';
  if (
$error = form_get_error($element) AND $error == t($message)) {
   
$key = implode('][', $element['#parents']);
   
$errors = &drupal_static('form_set_error');
    unset(
$errors[$key]);
   
_function_to_remove_drupal_message($message);
   
$state_validation_avoided = TRUE;
  }
}
/**
* Another element validation handler for Address Field countries.
*/
function my_module_country_element_validation_handler2($element, &$form_state, $form) {
  if (
drupal_static('scale_addressfield_state_validation_errors')) {
   
$form_state['rebuild'] = FALSE;
  }
}
?>

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

Or fork it on GitHub.

Comments

I might also recommend you

I might also recommend you could simply disable the AJAX functionality of the country field and use a generic fieldset with no adminsitrative area validation to work around these same issues. Some simple steps to reproduce the issues on a clean D7 install with Address Field would be helpful; in my comments interacting with the issue you linked, it appears I couldn't understand the nature of the error at the time and hadn't encountered it in any of my sites personally.

Ahh, you know, I was trying

Ahh, you know, I was trying to figure out why I wouldn't have encountered this in Drupal Commerce testing / development, and you nailed it. I do interact with the form element as an anonymous user, but I always have a session because I don't see the address field until I've added a product to the shopping cart. : )

I'll go back to the issue summary and get a local installation going with the issue. There's a lot of cleanup in general to be brought to the module, and it deserves the attention.

I've hit a similar issues

I've hit a similar issues with form and page cache colliding in weird ways, although not on an Addressfield. The issue I experienced was that when ajax magic happens, the form system attempts to merge cached values into the submitted ones. This isn't a problem as long as the field in question had a value during the ajax submission, but in the case of fields conditionally set to #access = FALSE, these would always get whatever values were in the form cache. And since cached pages naturally keep the same form token, the cached values for these forms are unreliable.

I found that if I used #type = hidden instead of #access = FALSE, the problem was avoided, since there was a $_GET or $_POST variable set for those elements, and the cached value did not overwrite the blank values that had been submitted. I don't know if that's a perfect solution for Addressfield, but it's probably worth exploring over invalidating the page cache on every submission or attempting to detect and manually deal with an "Illegal choice" error.

Add new comment