jenkins-bot has submitted this change and it was merged. Change subject: Mixed storage for buckets ......................................................................
Mixed storage for buckets Allows fallback to cookies for users without LocalStorage, in any campaign. Migrates legacy 'CN' cookies, and removes support for legacy legacy 'centralnotice_buckets_by_campaign' cookies. Bug: T132639 Change-Id: Icb0012a8daa2fa513a3f954507c0d7adb4ce4ae7 --- M CentralNotice.modules.php M resources/subscribing/ext.centralNotice.display.bucketer.js 2 files changed, 100 insertions(+), 59 deletions(-) Approvals: Awight: Checked; Looks good to me, approved jenkins-bot: Verified diff --git a/CentralNotice.modules.php b/CentralNotice.modules.php index 2f3ec05..cde693e 100644 --- a/CentralNotice.modules.php +++ b/CentralNotice.modules.php @@ -222,6 +222,7 @@ 'styles' => 'subscribing/ext.centralNotice.display.css', 'dependencies' => array( 'ext.centralNotice.geoIP', + 'ext.centralNotice.kvStore', 'jquery.cookie', 'json', 'mediawiki.Uri', diff --git a/resources/subscribing/ext.centralNotice.display.bucketer.js b/resources/subscribing/ext.centralNotice.display.bucketer.js index fabda8d..8ed7351 100644 --- a/resources/subscribing/ext.centralNotice.display.bucketer.js +++ b/resources/subscribing/ext.centralNotice.display.bucketer.js @@ -2,9 +2,10 @@ * Storage, retrieval and other processing of buckets. Provides * cn.internal.bucketer. * - * Bucket assignments are stored in a cookie named simply 'CN', to maximize - * concision. It consists of '*'-separated campaigns, each of which is made up - * for '!'-separated fields. The format is: + * Bucket assignments are stored using the kvStore, in LocalStorage or a cookie. + * To maximize concision for users that fall back to cookies, the value consists + * of '*'-separated campaigns, each of which is made up of '!'-separated fields. + * The format is: * * NAME!START!END!VALUE[*NAME!START!END!VALUE..] * @@ -14,26 +15,32 @@ * * For example: * - * CN=WikiConference_USA!39942400!3729600 + * 'WikiConference_USA!39942400!3729600!2' * * ...would be deserialized to: * - * { WikiConference_USA: { start: 1439942400, end: 1443672000 } } + * { WikiConference_USA: { start: 1439942400, end: 1443672000, val: 2 } } * */ ( function ( $, mw ) { - // Name of the old (pre-I2b39d153b) cookie for CentralNotice buckets. - // Its value is a JSON-encoded object, mapping campaign names to plain - // objects with 'start', 'end', and 'val' parameters. - var LEGACY_COOKIE = 'centralnotice_buckets_by_campaign', - - // Bucket objects by campaign; properties are campaign names. - // Retrieved from bucket cookie, if available. - buckets = null, + // Bucket objects by campaign; properties are campaign names. + // Retrieved from kvStore (which uses LocalStorage or a fallback cookie) + // or from a legacy cookie. + var buckets = null, // The campaign we're working with. - campaign = null; + campaign = null, + + kvStore = mw.centralNotice.kvStore, + multiStorageOption, + + // Name of the legacy cookie for CentralNotice buckets. Its value is + // a compact serialization of buckets in the the same format as is + // currently used here. + LEGACY_COOKIE = 'CN', + + STORAGE_KEY = 'buckets'; /** * Escape '*' and '!' in a campaign name to make it safe for serialization. @@ -53,36 +60,11 @@ } ); } - /** - * Attempt to get buckets from the bucket cookie. If there is no - * bucket cookie, check for a 'legacy cookie' (i.e., a cookie with - * the name and format used prior to I2b39d153b); if there is one, - * migrate it to the newer cookie format. If neither cookie exists, - * set buckets to an empty object. - */ - function loadBuckets() { - var cookieVal = $.cookie( 'CN' ); + function parseSerializedBuckets( serialized ) { - buckets = {}; + var parsedBuckets = {}; - if ( !cookieVal ) { - // Prior to I2b39d153be, the campaign cookie had a different - // (longer) name and used JSON encoding. If the user has such - // a cookie, migrate it to the new format. - cookieVal = $.cookie( LEGACY_COOKIE ); - if ( cookieVal ) { - $.removeCookie( LEGACY_COOKIE, { path: '/' } ); - try { - $.extend( buckets, JSON.parse( cookieVal ) ); - } catch ( e ) {} - if ( !$.isEmptyObject( buckets ) ) { - storeBuckets(); - } - } - return; - } - - $.each( cookieVal.split( '*' ), function ( idx, strBucket ) { + $.each( serialized.split( '*' ), function ( idx, strBucket ) { var parts = strBucket.split( '!' ), key = decodeCampaignName( parts[0] ), start = parseInt( parts[1], 10 ) + 14e8, @@ -90,22 +72,65 @@ val = parseInt( parts[3], 10 ); if ( key && start && end && !isNaN( val ) ) { - buckets[ key ] = { + parsedBuckets[ key ] = { start: start, end: end, val: val }; } } ); + + return parsedBuckets; } /** - * Store buckets in the bucket cookie. The cookie will be set to expire - * after the all the buckets it contains do. + * Check legacy bucket cookie, and try to migrate. If a legacy cookie is + * found, load buckets from there. + * + * @returns {boolean} true if a legacy cookie was migrated, false if not + */ + function possiblyLoadAndMigrateLegacyBuckets() { + + var cookieVal = $.cookie( LEGACY_COOKIE ); + + if ( cookieVal ) { + + // We need to deserialize and store again to determine ttl + buckets = parseSerializedBuckets( cookieVal ); + storeBuckets(); + $.removeCookie( LEGACY_COOKIE, { path: '/' } ); + return true; + } + + return false; + } + + /** + * Attempt to get buckets from the storage. If no stored buckets are + * found, set buckets to an empty object. + */ + function loadBuckets() { + + var val = kvStore.getItem( + STORAGE_KEY, + kvStore.contexts.GLOBAL, + multiStorageOption + ); + + buckets = ( val ? parseSerializedBuckets( val ) : {} ); + } + + /** + * Store buckets using the kvStore. The storage item will be set to + * expire after the all the buckets it contains do. + * + * Though compact serialization is no longer needed for the majority + * of users, who'll get LocalStorage, it's useful for those who fall + * back to cookies, and it seems preferable to keep things consistent. */ function storeBuckets() { var expires = Math.ceil( ( new Date() ) / 1000 ), - cookieVal = $.map( buckets, function ( opts, key ) { + serialized = $.map( buckets, function ( opts, key ) { var parts = [ escapeCampaignName( key ), Math.floor( opts.start - 14e8 ), @@ -120,11 +145,14 @@ return parts.join( '!' ); } ).join( '*' ); - // Store the buckets in the cookie - $.cookie( 'CN', cookieVal, { - expires: new Date( expires * 1000 ), - path: '/' - } ); + kvStore.setItem( + STORAGE_KEY, + serialized, + kvStore.contexts.GLOBAL, + // Convert expires to ttl in days + Math.ceil( ( expires - ( new Date() ) / 1000 ) / 86400 ), + multiStorageOption + ); } /** @@ -141,11 +169,11 @@ /** * Do all things bucket: - * - Get buckets from the cookie, if available. + * - Get buckets from the kvStore or legacy cookie, if available. * - If necessary, generate a random bucket for the campaign. * - Ensure the stored end date for this campaign is up-to-date. * - Go through all the buckets, purging expired buckets. - * - Store the updated bucket data in the cookie. + * - Store the updated bucket data using the kvStore. * * This should be called before a bucket is requested but after * setCampaign() has been called. @@ -168,17 +196,29 @@ bucketEndDate.setTime( campaign.end * 1000 ); bucketEndDate.setUTCDate( bucketEndDate.getUTCDate() + extension ); - loadBuckets(); + // Check if and how we can store buckets. Allow cookie fallback in all + // cases in which localStorage isn't available. + + // If we have no storage options (cookies and localStorage disabled), + // loading and storing buckets will be no-ops, and a new bucket will be + // chosen every time. + multiStorageOption = kvStore.getMultiStorageOption( true ); + + // In all cases, check for a legacy cookie and try to migrate if one + // was found. Otherwise, load normally. + if ( !possiblyLoadAndMigrateLegacyBuckets() ) { + loadBuckets(); + } + bucket = buckets[campaignName]; // If we have a valid bucket, just check and possibly update its // expiry. - // Note that buckets that are expired but that are found in - // the cookie (because they didn't have the chance to get - // purged) are not considered valid. In that case, for - // consistency, we choose a new random bucket, just as if - // no bucket had been found. + // Note that buckets that are expired but that were retrieved from + // storage (because they didn't have the chance to get purged) are + // not considered valid. In that case, for consistency, we choose a + // new random bucket, just as if no bucket had been found. if ( bucket && bucketEndDate > now ) { -- To view, visit https://gerrit.wikimedia.org/r/285571 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Icb0012a8daa2fa513a3f954507c0d7adb4ce4ae7 Gerrit-PatchSet: 7 Gerrit-Project: mediawiki/extensions/CentralNotice Gerrit-Branch: master Gerrit-Owner: AndyRussG <andrew.green...@gmail.com> Gerrit-Reviewer: AndyRussG <andrew.green...@gmail.com> Gerrit-Reviewer: Awight <awi...@wikimedia.org> Gerrit-Reviewer: Cdentinger <cdentin...@wikimedia.org> Gerrit-Reviewer: Ejegg <eeggles...@wikimedia.org> Gerrit-Reviewer: Ssmith <ssm...@wikimedia.org> Gerrit-Reviewer: XenoRyet <dkozlow...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits