{"id":111081,"date":"2025-04-18T07:00:43","date_gmt":"2025-04-18T14:00:43","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111081"},"modified":"2025-04-12T17:41:22","modified_gmt":"2025-04-13T00:41:22","slug":"the-case-of-the-feature-flag-that-didnt-stay-on-long-enough-part-2","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250418-43\/?p=111081","title":{"rendered":"The case of the feature flag that didn&#8217;t stay on long enough, part 2"},"content":{"rendered":"<p>Last time, <a title=\"The case of the feature flag that didn't stay on long enough, part 1\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250417-00\/?p=111079\"> we looked at a crash related to applying a temporary override of a feature flag<\/a>, but our first attempt to fix it didn&#8217;t help.<\/p>\n<p>If you look closely at the call stack for the crash, and read from the bottom up to follow execution in chronological order, we first have the TAEF command execution thread freeing the unit test library, which starts tearing down global variables, and it has reached the <code>WidgetRouter<\/code> singleton:<\/p>\n<pre style=\"white-space: pre-wrap;\">unittests!WidgetRouter::GetInstance'::`2'::`dynamic atexit destructor for 'singleton''\r\n<\/pre>\n<p>The <code>WidgetRouter<\/code> apparently keeps a vector of objects that need closing, and one of them is another Widget, on which it dutifully calls <code>Close<\/code>. The <code>Widget::<wbr \/>Close<\/code> method asks for the singleton <code>WidgetRouter<\/code>, and now the assertion fails because the feature override is not in effect.<\/p>\n<p>The feature override was released at the end of the unit test function, and that finished a long time ago. TAEF is trying to clean up after all the tests have completed.<\/p>\n<p>The problem is that the Widget was created while the feature flag was set (due to the override), but it is being closed while the feature flag is clear (because the override has been lifted). This is a &#8220;torn state&#8221; situation that generally makes nobody happy.<\/p>\n<p>To avoid torn state, you have find everything that changes behavior based on the feature flag and tear it down or at least put it back into a state that is not dependent on the feature flag, and do this before you change the feature flag.<\/p>\n<p>One solution for the purpose of this unit test is to override the <code>Widget\u00adRouter::<wbr \/>Get\u00adInstance<\/code> method to return a custom <code>Widget\u00adRouter<\/code> for the state where the feature flag is enabled. Running it down at the end of the function allows everything to clean up while the override is still in place, thereby avoiding the torn state problem.<\/p>\n<p>Here&#8217;s one possible implementation:<\/p>\n<pre>\/* static method *\/\r\nstd::shared_ptr&lt;WidgetRouter&gt;\r\n    WidgetRouter::GetInstance()\r\n{\r\n    <span style=\"border: solid 1px currentcolor; border-bottom: none;\">auto result = UnitTest_Override_WidgetRouter_GetInstance();<\/span>\r\n    <span style=\"border: solid 1px currentcolor; border-top: none;\">if (result) return *result;                                <\/span>\r\n\r\n    assert(FeatureFlags::IsEnabled(NewFeatureId));\r\n\r\n    static std::shared_ptr&lt;WidgetRouter&gt; singleton =\r\n        std::make_shared&lt;WidgetRouter&gt;();\r\n\r\n    return singleton;\r\n}\r\n<\/pre>\n<p>where the production code has a stub implementation of the unit test override (that always returns <code>nullptr<\/code>), and the unit test has an implementation that returns a special <code>WidgetRouter<\/code> for unit testing purposes.<\/p>\n<pre>std::shared_ptr&lt;WidgetRouter&gt; g_unitTestRouter;\r\n\r\nstd::shared_ptr&lt;WidgetRouter&gt;* UnitTest_Override_WidgetRouter_GetInstance()\r\n{\r\n    return std::addressof(g_unitTestRouter);\r\n}\r\n\r\nvoid WidgetTests::BasicTests()\r\n{\r\n  \/\/ Force the feature flag on for the duration of this test\r\n  auto override = std::make_unique&lt;FeatureOverride&gt;(NewFeatureId, true);\r\n\r\n  <span style=\"border: solid 1px currentcolor; border-bottom: none;\">\/\/ Create our custom widget router for this unit test.                <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">\/\/ This router's lifetime is enclosed by the lifetime of the          <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">\/\/ feature flag override.                                             <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">g_unitTestRouter = std::make_shared&lt;WidgetRouter&gt;();                  <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">\u00a0                                                                     <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">auto cleanup = wil::scope_exit([] {                                   <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">  \/\/ Destroy the custom widget router. This will run it down while the<\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">  \/\/ feature flag override is still in effect.                        <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">  g_unitTestRouter = nullptr;                                         <\/span>\r\n  <span style=\"border: solid 1px currentcolor; border-top: none;\">});                                                                   <\/span>\r\n\r\n  auto widget = winrt::Component::Widget();\r\n  \u27e6 test the widget in various ways \u27e7\r\n}\r\n<\/pre>\n<p>Here, we tell <code>Widget\u00adRouter::<wbr \/>Get\u00adInstance()<\/code> to use our <code>g_unitTestRouter<\/code> instead of its private one. That way, we can run down the <code>Widget\u00adRouter<\/code> while the feature flag override is still in effect and avoid torn state.<\/p>\n<p>But there&#8217;s a simpler solution: Give the unit test a way to force the singleton <code>Widget\u00adRouter<\/code> to run down.<\/p>\n<pre>static std::shared_ptr&lt;WidgetRouter&gt; g_singletonWidgetRouter;\r\n\r\n\/* static method *\/\r\nstd::shared_ptr&lt;WidgetRouter&gt;\r\n    WidgetRouter::GetInstance()\r\n{\r\n    assert(FeatureFlags::IsEnabled(NewFeatureId));\r\n\r\n    static bool init = [] {\r\n        g_singletonWidgetRouter = std::make_shared&lt;WidgetRouter&gt;();\r\n    }();\r\n\r\n    return g_singletonWidgetRouter;\r\n}\r\n\r\n\/* static method *\/\r\nvoid WidgetRouter::ResetInstance_ForUnitTestOnly()\r\n{\r\n    g_singletonWidgetRouter = nullptr;\r\n}\r\n\r\nvoid WidgetTests::BasicTests()\r\n{\r\n  \/\/ Force the feature flag on for the duration of this test\r\n  auto override = std::make_unique&lt;FeatureOverride&gt;(NewFeatureId, true);\r\n\r\n  <span style=\"border: solid 1px currentcolor; border-bottom: none;\">\/\/ Clean up the widget router before the override goes<\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">\/\/ out of scope.                                      <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">auto cleanup = wil::scope_exit([] {                   <\/span>\r\n  <span style=\"border: 1px currentcolor; border-style: none solid;\">  WidgetRouter::ResetInstance_ForUnitTestOnly();      <\/span>\r\n  <span style=\"border: solid 1px currentcolor; border-top: none;\">});                                                   <\/span>\r\n\r\n  auto widget = winrt::Component::Widget();\r\n  \u27e6 test the widget in various ways \u27e7\r\n}\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Leaving everything the way you found it.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-111081","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Leaving everything the way you found it.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111081","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=111081"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111081\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=111081"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111081"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111081"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}