-
Notifications
You must be signed in to change notification settings - Fork 123
Add variants for feature flags #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
85 commits
Select commit
Hold shift + click to select a range
d28bf88
in progress, add new classes for variants and define methods in featu…
amerjusupovic c1451d3
Revert "Revert "Add cancellation token parameter to async feature man…
amerjusupovic d087e7b
Revert "Revert "Added default value for cancellation token in interfa…
amerjusupovic 87aa5a8
fix any conflicts left from adding cancellationToken back
amerjusupovic 55e2264
add in progress changes to allocation and featuredefinitionprovider
amerjusupovic 1918bf9
add examples for testing
amerjusupovic 3e3ab2e
fix adding new featuredefinition properties from featuremanagement de…
amerjusupovic 703f89a
progress adding getvariant logic classes
amerjusupovic e225d22
continued
amerjusupovic c233ec6
remove repeated code in contextual targeting
amerjusupovic 3a8bac1
fix version of contextual filter
amerjusupovic fa2133c
more progress on getting the contextual allocator to work
amerjusupovic 86ff346
about to test getvariant
amerjusupovic c445e46
add example to test
amerjusupovic cd09ec2
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic 0a4dcfb
add snapshot changes
amerjusupovic 82055ad
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic 34375e6
variant can be detected and retrieved from getvariantasync
amerjusupovic 1f6318b
progress on allocation logic, add comments where consideration needed
amerjusupovic 1f76adc
add use of optionsresolver for reference, todo work on isenabledasync…
amerjusupovic ee98bbe
All working except couple TODOs, need to add unit tests
amerjusupovic 42a7cbc
remove some comments, add null check where needed
amerjusupovic 3df6f32
update todo comments
amerjusupovic cbc322e
fix line eols
amerjusupovic 7e801d9
add unit test, in progress
amerjusupovic ee3fdb0
TODOs in progress, need to restructure featurevariantassigner design
amerjusupovic 76ad51f
fix seed logic
amerjusupovic b7a97a7
update comments, status logic
amerjusupovic 370228e
remove unnecessary files for custom assigners, fix featuremanager met…
amerjusupovic a259e23
fix naming from allocator to assigner for classes and files
amerjusupovic 2850421
cleanup extra methods, todo config section logic
amerjusupovic 96b636d
in progress adding configurationsection returned when using configura…
amerjusupovic 43e1b13
continuation of last commit
amerjusupovic 5f6328e
working return for configvalue
amerjusupovic fbc20fe
move logic to featuremanager for assigning
amerjusupovic 46a263e
remove unused assigner classes
amerjusupovic baac628
add new configurationsection to handle return for variant
amerjusupovic 6672af6
null error, in progress new configurationsection class
amerjusupovic 4d6064b
fix old bug
amerjusupovic 6012ec3
progress on unit tests
amerjusupovic d2543ea
more null check changes, test fixes
amerjusupovic 8d59200
reset examples changes
amerjusupovic 77fc24b
Revert "Revert "Revert "Added default value for cancellation token in…
amerjusupovic a286d43
Revert "Revert "Revert "Add cancellation token parameter to async fea…
amerjusupovic 9bbd115
add comments for new classes
amerjusupovic 3ea099b
fix comments for public classes again
amerjusupovic 8306eb4
update comments, default values
amerjusupovic 8765c93
fix variantconfigurationsection, comments in definitionprovider
amerjusupovic a3466c7
fix using statements, null checks
amerjusupovic eb80694
fix unit test failures with servicecollectionextensions
amerjusupovic 0b8f9e9
add revisions: fix namepaces, add exceptions tests, combine percentag…
amerjusupovic 1da4d2a
change context accessor logic
amerjusupovic dff6044
fix comments for default variants
amerjusupovic adbf40f
PR revisions
amerjusupovic 875a422
change class names, PR fixes
amerjusupovic 05b31b1
Merge branch 'main' of https://github.com/microsoft/FeatureManagement…
amerjusupovic c50c96e
fix edge case percentage targeting
amerjusupovic 95e6459
rename allocation classes, remove exceptions and add warning logs, pr…
amerjusupovic 744b966
refactor isenabled to remove boolean param
amerjusupovic 02f6e55
change configurationvalue to IConfigurationSection instead of string
amerjusupovic efbab17
fix enabledwithvariants logic
amerjusupovic e4cddae
PR revisions, fix logic in new methods from last commit
amerjusupovic e8a640b
set session managers last in flow
amerjusupovic dc49a2f
make false explicit for status disabled or missing definition
amerjusupovic d184882
fix constructor default params, move session managers logic, pr revis…
amerjusupovic 49aa2fb
fix comment
amerjusupovic a0a787a
fix resolvedefaultvariant, isexternalinit error
amerjusupovic 6ef1fce
add back 3.1
amerjusupovic 2067d27
Apply suggestions from code review
amerjusupovic 3fa5f53
isexternalinit comments, remove resolvedefault helper
amerjusupovic c745f59
remove binding, fix featuredefinitionprovider issues
amerjusupovic de67697
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic 29bce04
change to Debug.Assert from Assert
amerjusupovic 9c7765d
update method name
amerjusupovic 1fb1c67
remove parseenum, add ConfigurationFields class
amerjusupovic 6dfb3ec
test failing, fixed PR revisions
amerjusupovic 827c0ae
fix invalid scenarios test
amerjusupovic 656ec67
simplify context in test
amerjusupovic bed8093
remove unused using
amerjusupovic 03e8e47
remove unused param
amerjusupovic e1cb0d2
Clarify how From and To bounds work in PercentileAllocation
amerjusupovic 23ff1ef
fix error messages
amerjusupovic 2340b8c
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic 3827494
add feature name as default seed with allocation prefix
amerjusupovic 755687e
Update src/Microsoft.FeatureManagement/FeatureManager.cs
amerjusupovic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| /// <summary> | ||
| /// The definition of how variants are allocated for a feature. | ||
| /// </summary> | ||
| public class Allocation | ||
| { | ||
| /// <summary> | ||
| /// The default variant used if the feature is enabled and no variant is assigned. | ||
| /// </summary> | ||
| public string DefaultWhenEnabled { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The default variant used if the feature is disabled. | ||
| /// </summary> | ||
| public string DefaultWhenDisabled { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Describes a mapping of user ids to variants. | ||
| /// </summary> | ||
| public IEnumerable<UserAllocation> User { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Describes a mapping of group names to variants. | ||
| /// </summary> | ||
| public IEnumerable<GroupAllocation> Group { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Allocates percentiles of user base to variants. | ||
| /// </summary> | ||
| public IEnumerable<PercentileAllocation> Percentile { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Maps users to the same percentile across multiple feature flags. | ||
| /// </summary> | ||
| public string Seed { get; set; } | ||
| } | ||
| } | ||
24 changes: 24 additions & 0 deletions
24
src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
|
|
||
| using System.Collections.Generic; | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| /// <summary> | ||
| /// The definition of a group allocation. | ||
| /// </summary> | ||
| public class GroupAllocation | ||
| { | ||
| /// <summary> | ||
| /// The name of the variant. | ||
| /// </summary> | ||
| public string Variant { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// A list of groups that can be assigned this variant. | ||
| /// </summary> | ||
| public IEnumerable<string> Groups { get; set; } | ||
| } | ||
| } |
27 changes: 27 additions & 0 deletions
27
src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| /// <summary> | ||
| /// The definition of a percentile allocation. | ||
| /// </summary> | ||
| public class PercentileAllocation | ||
| { | ||
| /// <summary> | ||
| /// The name of the variant. | ||
| /// </summary> | ||
| public string Variant { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The inclusive lower bound of the percentage to which the variant will be assigned. | ||
| /// </summary> | ||
| public double From { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The exclusive upper bound of the percentage to which the variant will be assigned. | ||
| /// </summary> | ||
| public double To { get; set; } | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
|
|
||
| using System.Collections.Generic; | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| /// <summary> | ||
| /// The definition of a user allocation. | ||
| /// </summary> | ||
| public class UserAllocation | ||
| { | ||
| /// <summary> | ||
| /// The name of the variant. | ||
| /// </summary> | ||
| public string Variant { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// A list of users that will be assigned this variant. | ||
| /// </summary> | ||
| public IEnumerable<string> Users { get; set; } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
@@ -21,13 +22,13 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider | |
| // IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's | ||
| // provider to be marked for caching as well. | ||
|
|
||
| private const string FeatureFiltersSectionName = "EnabledFor"; | ||
| private const string RequirementTypeKeyword = "RequirementType"; | ||
| private readonly IConfiguration _configuration; | ||
| private readonly ConcurrentDictionary<string, FeatureDefinition> _definitions; | ||
| private IDisposable _changeSubscription; | ||
| private int _stale = 0; | ||
|
|
||
| const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'."; | ||
|
|
||
| public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) | ||
| { | ||
| _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); | ||
|
|
@@ -136,13 +137,19 @@ We support | |
|
|
||
| RequirementType requirementType = RequirementType.Any; | ||
|
|
||
| FeatureStatus featureStatus = FeatureStatus.Conditional; | ||
|
|
||
| Allocation allocation = null; | ||
|
|
||
| List<VariantDefinition> variants = null; | ||
|
|
||
| var enabledFor = new List<FeatureFilterConfiguration>(); | ||
|
|
||
| string val = configurationSection.Value; // configuration[$"{featureName}"]; | ||
|
|
||
| if (string.IsNullOrEmpty(val)) | ||
| { | ||
| val = configurationSection[FeatureFiltersSectionName]; | ||
| val = configurationSection[ConfigurationFields.FeatureFiltersSectionName]; | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(val) && bool.TryParse(val, out bool result) && result) | ||
|
|
@@ -160,57 +167,173 @@ We support | |
| } | ||
| else | ||
| { | ||
| string rawRequirementType = configurationSection[RequirementTypeKeyword]; | ||
| string rawRequirementType = configurationSection[ConfigurationFields.RequirementType]; | ||
|
|
||
| // | ||
| // If requirement type is specified, parse it and set the requirementType variable | ||
| if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) | ||
| string rawFeatureStatus = configurationSection[ConfigurationFields.FeatureStatus]; | ||
|
|
||
| if (!string.IsNullOrEmpty(rawRequirementType)) | ||
| { | ||
| throw new FeatureManagementException( | ||
| FeatureManagementError.InvalidConfigurationSetting, | ||
| $"Invalid requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'."); | ||
| requirementType = ParseEnum<RequirementType>(configurationSection.Key, rawRequirementType, ConfigurationFields.RequirementType); | ||
| } | ||
|
|
||
| IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); | ||
| if (!string.IsNullOrEmpty(rawFeatureStatus)) | ||
| { | ||
| featureStatus = ParseEnum<FeatureStatus>(configurationSection.Key, rawFeatureStatus, ConfigurationFields.FeatureStatus); | ||
| } | ||
|
|
||
| IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren(); | ||
|
|
||
| foreach (IConfigurationSection section in filterSections) | ||
| { | ||
| // | ||
| // Arrays in json such as "myKey": [ "some", "values" ] | ||
| // Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" } | ||
| if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)])) | ||
| if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) | ||
| { | ||
| enabledFor.Add(new FeatureFilterConfiguration() | ||
| { | ||
| Name = section[nameof(FeatureFilterConfiguration.Name)], | ||
| Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters))) | ||
| Name = section[ConfigurationFields.NameKeyword], | ||
| Parameters = new ConfigurationWrapper(section.GetSection(ConfigurationFields.FeatureFilterConfigurationParameters)) | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| IConfigurationSection allocationSection = configurationSection.GetSection(ConfigurationFields.AllocationSectionName); | ||
|
|
||
| if (allocationSection.Exists()) | ||
| { | ||
| allocation = new Allocation() | ||
| { | ||
| DefaultWhenDisabled = allocationSection[ConfigurationFields.AllocationDefaultWhenDisabled], | ||
| DefaultWhenEnabled = allocationSection[ConfigurationFields.AllocationDefaultWhenEnabled], | ||
| User = allocationSection.GetSection(ConfigurationFields.UserAllocationSectionName).GetChildren().Select(userAllocation => | ||
| { | ||
| return new UserAllocation() | ||
| { | ||
| Variant = userAllocation[ConfigurationFields.AllocationVariantKeyword], | ||
| Users = userAllocation.GetSection(ConfigurationFields.UserAllocationUsers).Get<IEnumerable<string>>() | ||
| }; | ||
| }), | ||
| Group = allocationSection.GetSection(ConfigurationFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation => | ||
| { | ||
| return new GroupAllocation() | ||
| { | ||
| Variant = groupAllocation[ConfigurationFields.AllocationVariantKeyword], | ||
| Groups = groupAllocation.GetSection(ConfigurationFields.GroupAllocationGroups).Get<IEnumerable<string>>() | ||
| }; | ||
| }), | ||
| Percentile = allocationSection.GetSection(ConfigurationFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation => | ||
| { | ||
| double from = 0; | ||
|
|
||
| double to = 0; | ||
|
|
||
| string rawFrom = percentileAllocation[ConfigurationFields.PercentileAllocationFrom]; | ||
|
|
||
| string rawTo = percentileAllocation[ConfigurationFields.PercentileAllocationTo]; | ||
|
|
||
| if (!string.IsNullOrEmpty(rawFrom)) | ||
| { | ||
| from = ParseDouble(configurationSection.Key, rawFrom, ConfigurationFields.PercentileAllocationFrom); | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(rawTo)) | ||
| { | ||
| to = ParseDouble(configurationSection.Key, rawTo, ConfigurationFields.PercentileAllocationTo); | ||
| } | ||
|
|
||
| return new PercentileAllocation() | ||
| { | ||
| Variant = percentileAllocation[ConfigurationFields.AllocationVariantKeyword], | ||
| From = from, | ||
| To = to | ||
| }; | ||
| }), | ||
| Seed = allocationSection[ConfigurationFields.AllocationSeed] | ||
| }; | ||
| } | ||
|
|
||
| IEnumerable<IConfigurationSection> variantsSections = configurationSection.GetSection(ConfigurationFields.VariantsSectionName).GetChildren(); | ||
| variants = new List<VariantDefinition>(); | ||
|
|
||
| foreach (IConfigurationSection section in variantsSections) | ||
| { | ||
| if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) | ||
| { | ||
| StatusOverride statusOverride = StatusOverride.None; | ||
|
|
||
| string rawStatusOverride = section[ConfigurationFields.VariantDefinitionStatusOverride]; | ||
|
|
||
| if (!string.IsNullOrEmpty(rawStatusOverride)) | ||
| { | ||
| statusOverride = ParseEnum<StatusOverride>(configurationSection.Key, rawStatusOverride, ConfigurationFields.VariantDefinitionStatusOverride); | ||
| } | ||
|
|
||
| VariantDefinition variant = new VariantDefinition() | ||
| { | ||
| Name = section[ConfigurationFields.NameKeyword], | ||
| ConfigurationValue = section.GetSection(ConfigurationFields.VariantDefinitionConfigurationValue), | ||
| ConfigurationReference = section[ConfigurationFields.VariantDefinitionConfigurationReference], | ||
| StatusOverride = statusOverride | ||
| }; | ||
|
|
||
| variants.Add(variant); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return new FeatureDefinition() | ||
| { | ||
| Name = configurationSection.Key, | ||
| EnabledFor = enabledFor, | ||
| RequirementType = requirementType | ||
| RequirementType = requirementType, | ||
| Status = featureStatus, | ||
| Allocation = allocation, | ||
| Variants = variants | ||
| }; | ||
| } | ||
|
|
||
| private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections() | ||
| { | ||
| const string FeatureManagementSectionName = "FeatureManagement"; | ||
|
|
||
| if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) | ||
| if (_configuration.GetChildren().Any(s => s.Key.Equals(ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| // | ||
| // Look for feature definitions under the "FeatureManagement" section | ||
| return _configuration.GetSection(FeatureManagementSectionName).GetChildren(); | ||
| return _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName).GetChildren(); | ||
| } | ||
| else | ||
| { | ||
| return _configuration.GetChildren(); | ||
| } | ||
| } | ||
|
|
||
| private T ParseEnum<T>(string feature, string rawValue, string fieldKeyword) | ||
| where T: struct, Enum | ||
| { | ||
| Debug.Assert(!string.IsNullOrEmpty(rawValue)); | ||
|
|
||
| if (!Enum.TryParse(rawValue, ignoreCase: true, out T value)) | ||
| { | ||
| throw new FeatureManagementException( | ||
| FeatureManagementError.InvalidConfigurationSetting, | ||
| string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature)); | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
|
|
||
| private double ParseDouble(string feature, string rawValue, string fieldKeyword) | ||
| { | ||
| Debug.Assert(!string.IsNullOrEmpty(rawValue)); | ||
|
|
||
| if (!double.TryParse(rawValue, out double value)) | ||
| { | ||
| throw new FeatureManagementException( | ||
| FeatureManagementError.InvalidConfigurationSetting, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The knowledge that configurationSection.Key is feature name is a bit of odd encapsulation. I'd suggest updating the method signature. |
||
| string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature)); | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.