巨大なアプリ開発を支えるフラグ管理術 / Flag-based feature management

巨大なアプリ開発を支えるフラグ管理術 / Flag-based feature management

Munetoshi Ishikawa (石川宗寿)
LINE Corporation Senior Software Engineer

DroidKaigi 2019
https://droidkaigi.jp/2019/timetable

53850955f15249a1a9dc49df6113e400?s=128

LINE Developers

February 08, 2019
Tweet

Transcript

  1. ڊେͳΞϓϦ։ൃΛࢧ͑Δϑϥά؅ཧज़
 Flag-based feature management for a large scale application Abstract

    in three lines: 1. Branches are hard to manage in a large application 2. Enables/disables features in a property file 3. Implemented as a Gradle plugin Slides: in English
 Talk: in Japanese
  2. Flag-based feature management for a large scale application Munetoshi Ishikawa

  3. What I’d Like to Say For a large scale application

    ‣ Hard to manage lots of feature branches ‣ Hard to conduct “large” refactoring
  4. What I’d Like to Say For a large scale application

    ‣ Hard to manage lots of feature branches ‣ Hard to conduct “large” refactoring Go with feature flags, no more feature branches
  5. Topics ‣ Pain of feature branches ‣ Overview of feature

    flags ‣ Flag implementation ‣ Limitations and considerations
  6. Topics ‣ Pain of feature branches ‣ Overview of feature

    flags ‣ Flag implementation ‣ Limitations and considerations
  7. Feature Branch Example: git-flow, GitHub flow main branch (trunk, master,

    develop…)
  8. Feature Branch Example: git-flow, GitHub flow feature branch main branch

    (trunk, master, develop…)
  9. Feature Branch Example: git-flow, GitHub flow feature branch pull request

    main branch (trunk, master, develop…)
  10. Feature Branch Example: git-flow, GitHub flow feature branch pull request

    main branch (trunk, master, develop…)
  11. Case of Large Scale Application Lots of branches for a

    large scale application main branch feature branch
  12. What’s the Problem? Conflicts, conflicts, conflicts… - Commonly used code

    updated, like an API/library - Project wide refactoring, like package renaming feature development main branch …
  13. What’s the Problem? Conflicts, conflicts, conflicts… - Commonly used code

    updated, like an API/library - Project wide refactoring, like package renaming feature development main branch … refactoring
  14. What’s the Problem? Conflicts, conflicts, conflicts… - Commonly used code

    updated, like an API/library - Project wide refactoring, like package renaming feature development main branch … refactoring Conflict!
  15. How to Resolve Conflicts 1/2 Merge refactoring first feature development

    main branch …
  16. How to Resolve Conflicts 1/2 Merge refactoring first feature development

    main branch … refactoring
  17. How to Resolve Conflicts 1/2 Merge refactoring first feature development

    main branch … refactoring resolve
  18. How to Resolve Conflicts 1/2 Merge refactoring first Many branches

    are required to resolve conflicts feature development main branch … refactoring resolve
  19. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch …
  20. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch … refactoring
  21. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch … refactoring resolve
  22. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch … refactoring resolve 1. The conflict becomes large
  23. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch … refactoring 1. The conflict becomes large 2. No chance to resolve with “all” branches resolve
  24. How to Resolve Conflicts 2/2 Merge feature development first feature

    development main branch … refactoring 1. The conflict becomes large 2. No chance to resolve with “all” branches resolve Conflict!
  25. How to Minimize the Conflicts Apply each “halfway” update to

    each other - Merge before finishing the implementation - Merge before fixing the release plan
  26. How to Minimize the Conflicts Apply each “halfway” update to

    each other - Merge before finishing the implementation - Merge before fixing the release plan Guard a “halfway” feature by a boolean flag
  27. Topics ‣ Pain of feature branches ‣ Overview of feature

    flags ‣ Flag implementation ‣ Limitations and considerations
  28. Feature Flag Concept ‣ Merge any pull request to the

    main branch main branch pull request
  29. Feature Flag Concept ‣ Merge any pull request to the

    main branch ‣ Enable/disable a feature with a property file main branch pull request flag property file FEATURE_STICKER=ENABLED FEATURE_THEME=DISABLED
  30. Feature Flag Usage Example Case1: Replace presentation logic presenter =

    if (BuildConfig.FEATURE_STICKER) NewPresenter() else OldPresenter()

  31. Feature Flag Usage Example Case1: Replace presentation logic presenter =

    if (BuildConfig.FEATURE_STICKER) NewPresenter() else OldPresenter()
 Case2: Change view visibility button.isVisible = BuildConfig.FEATURE_THEME
  32. What a Feature Flag Realizes ‣ Minimizes conflicts ‣ Separates

    release schedule from the development progress ‣ Disables features if critical issues found on the release branch ‣ Shares the halfway features to other members
  33. What a Feature Flag Realizes ‣ Minimizes conflicts ‣ Separates

    release schedule from the development progress ‣ Disables features if critical issues found on the release branch ‣ Shares the halfway features to other members
  34. Conflict Minimization Apply a project wide update to halfway features

    main branch feature PR
  35. Conflict Minimization Apply a project wide update to halfway features

    main branch feature PR refactoring PR
  36. Conflict Minimization Apply a project wide update to halfway features

    main branch feature PR refactoring PR Conflict
  37. Conflict Minimization Apply a project wide update to halfway features

    - Forget about ongoing refactoring once a pull request is merged main branch feature PR refactoring PR Conflict saved PRs
  38. What a Feature Flag Realizes ‣ Minimizes conflicts ‣ Separates

    release schedule from the development progress ‣ Disables features if critical issues found on the release branch ‣ Shares the halfway features to other members
  39. Release schedule Feature PRs can be merged before creating target

    release branch main branch release branch 1.0 2.0
  40. Release schedule Feature PRs can be merged before creating target

    release branch main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0)
  41. Release schedule Feature PRs can be merged before creating target

    release branch main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0) STICKER=DISABLED STICKER=ENABLED
  42. Release schedule Feature PRs can be merged before creating target

    release branch main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0) STICKER=DISABLED STICKER=ENABLED
  43. What a Feature Flag Realizes ‣ Minimizes conflicts ‣ Separates

    release schedule from the development progress ‣ Disables features if critical issues found on the release branch ‣ Shares the halfway features to other members
  44. Feature Disabling 1/2 Hard to revert after resolving conflict main

    branch release branch feature branch
  45. Feature Disabling 1/2 Hard to revert after resolving conflict main

    branch release branch problem found feature branch
  46. Feature Disabling 1/2 Hard to revert after resolving conflict main

    branch release branch refactoring PR problem found feature branch
  47. Feature Disabling 2/2 Just turn off problematic features problem found

    main branch release branch feature PR refactoring PR
  48. Feature Disabling 2/2 Just turn off problematic features STICKER=ENABLED DISABLED

    → problem found main branch release branch feature PR refactoring PR
  49. What a Feature Flag Realizes ‣ Minimizes conflicts ‣ Separates

    release schedule from the development progress ‣ Disables features if critical issues found on the release branch ‣ Shares the halfway features to other members
  50. Halfway Feature Sharing ‣ Code is visible for other developers

    - For a related or dependent feature development ‣ A feature is deployable for a demo and testing - For project owners, testers, and designers
  51. Topics ‣ Pain of feature branches ‣ Overview of feature

    flag ‣ Flag implementation ‣ Limitations and considerations
  52. Implementation Options ‣ Gradle plugin and BuildConfig ‣ Build variants

    ‣ Annotation processing ‣ Reflection
  53. Implementation Options ‣ Gradle plugin and BuildConfig ‣ Build variants

    ‣ Annotation processing ‣ Reflection
  54. Key Idea of Implementation 1. Create a Java property file

    2. Write the property content to BuildConfig FEATURE_STICKER=true
 FEATURE_THEME=false feature_flag.properties
  55. Key Idea of Implementation 1. Create a Java property file

    2. Write the property content to BuildConfig Properties()... .forEach { (key, value) -> variant .buildConfigField("boolean", "$key", "$value") } FeatureFlagPlugin.kt (Gradle plugin)
  56. Key Idea of Implementation 1. Create a Java property file

    2. Write the property content to BuildConfig public static final boolean FEATURE_STICKER = true; public static final boolean FEATURE_THEME = false; BuildConfig.java (output)
  57. Requirements ‣ IDE warnings workaround ‣ Flag control by build

    variants ‣ Flag control by release version ‣ Runtime flag modification
  58. Requirements ‣ IDE warnings workaround ‣ Flag control by build

    variants ‣ Flag control by release version ‣ Runtime flag modification
  59. IDE Warnings Warning for a conditional branch with a boolean

    constant if (BuildConfig.FEATURE_STICKER) {
  60. IDE Warning Workaround Call `Boolean.valueOf(...)` for a debug build 


    val valueString = "Boolean.valueOf($value)" FeatureFlagPlugin.kt (Gradle plugin)
  61. IDE Warning Workaround Call `Boolean.valueOf(...)` for a debug build (Use

    a literal for release to help ProGuard) val isRelease = variant.buildType.name == "release"
 val valueString = if (isRelease) "$value" else "Boolean.valueOf($value)" FeatureFlagPlugin.kt (Gradle plugin)
  62. Requirements ‣ IDE warnings workaround ‣ Flag control by build

    variants ‣ Flag control by release version ‣ Runtime flag modification
  63. Flag Control by Build Variants Enable with debug while disable

    with release # Enabled only for debug FEATURE_STICKER=debug # Enabled for both of debug and release FEATURE_THEME=release feature_flag.properties
  64. Implementation of Control by Variants 1/2 const val DEBUG_NAME =

    "debug" const val RELEASE_NAME = "release" val BUILD_TYPE_TO_FLAG_LIST_MAP = mapOf( DEBUG_NAME to arrayOf(DEBUG_NAME, RELE...), RELEASE_NAME to arrayOf(RELEASE_NAME) ) FeatureFlagPlugin.kt (Gradle plugin)
  65. Implementation of Control by Variants 2/2 val buildTypeName = variant.buildType.name

    FeatureFlagPlugin.kt (Gradle plugin)
  66. Implementation of Control by Variants 2/2 val buildTypeName = variant.buildType.name

    val availableFlagValues = BUILD_TYPE_TO_FLAG_LIST_MAP[buildTypeName] .orEmpty() FeatureFlagPlugin.kt (Gradle plugin)
  67. Implementation of Control by Variants 2/2 val buildTypeName = variant.buildType.name

    val availableFlagValues = BUILD_TYPE_TO_FLAG_LIST_MAP[buildTypeName] .orEmpty() val isEnabled = availableFlagValues.contains(flagValue) // WHERE, flagValue is "debug" or "release" FeatureFlagPlugin.kt (Gradle plugin)
  68. Requirements ‣ IDE warnings workaround ‣ Flag control by build

    variants ‣ Flag control by release version ‣ Runtime flag modification
  69. Flag Control by Release Version 1/2 May forget to enable

    before creating the target release branch main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0) STICKER=debug STICKER=release
  70. Flag Control by Release Version 1/2 May forget to enable

    before creating the target release branch main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0) STICKER=debug STICKER=release
  71. Flag Control by Release Version 2/2 Reserve target release version

    as soon as release schedule fixed main branch release branch 1.0 2.0 feature “STICKER” PRs (for 2.0) STICKER=debug,release:since_2.0
  72. Implementation of Control by Version val tokenizedFlag = flag.split(":since_")
 ...

    FeatureFlagPlugin.kt (Gradle plugin)
  73. Implementation of Control by Version val tokenizedFlag = flag.split(":since_")
 ...

    val sinceVersion = VersionNumber .parse(tokenizedFlag.getOrNull(1).orEmpty()) FeatureFlagPlugin.kt (Gradle plugin)
  74. Implementation of Control by Version val tokenizedFlag = flag.split(":since_")
 ...

    val sinceVersion = VersionNumber .parse(tokenizedFlag.getOrNull(1).orEmpty()) availableFlagValues.contains(buildTypeName) && sinceVersion <= project.applicationVersion FeatureFlagPlugin.kt (Gradle plugin)
  75. Bonus Points for Control by Version Easy to know when

    we can clean up the flag STICKER=...:since_2.0 <delete flag> main branch 2.0
  76. Requirements ‣ IDE warnings workaround ‣ Flag control by build

    variants ‣ Flag control by release version ‣ Runtime flag modification
  77. Runtime Flag Modification Make flags overridable in a debug menu

    for - Demoing - Debugging - Side-by-side testing
  78. Implementation of Flag Modification 1/2 `buildConfigField` outputs `static final` (I

    don’t like Java’s reflection. Especially, sun.misc.Unsafe) variant.buildConfigField( "boolean", "$key_DUMMY", "$value " ) FeatureFlagPlugin.kt (Gradle plugin)
  79. Implementation of Flag Modification 1/2 `buildConfigField` outputs `static final` (I

    don’t like Java’s reflection. Especially, sun.misc.Unsafe) variant.buildConfigField( "boolean", "$key_DUMMY", "$value; " ) FeatureFlagPlugin.kt (Gradle plugin)
  80. Implementation of Flag Modification 1/2 `buildConfigField` outputs `static final` (I

    don’t like Java’s reflection. Especially, sun.misc.Unsafe) variant.buildConfigField( "boolean", "$key_DUMMY", "$value; public static boolean $key = $value" ) FeatureFlagPlugin.kt (Gradle plugin)
  81. Implementation of Flag Modification 1/2 `buildConfigField` outputs `static final` (I

    don’t like Java’s reflection. Especially, sun.misc.Unsafe) public static final boolean FEATURE_STICKER_DUMMY = true; public static boolean FEATURE_STICKER = true; BuildConfig.java (output)
  82. Implementation of Flag Modification 2/2 Make overridable flag accessible without

    reflection - Create a flag list with supplier/consumer pair
  83. Implementation of Flag Modification 2/2 Make overridable flag accessible without

    reflection - Create a flag list with supplier/consumer pair val codeBuilder = StringJoiner("\n", "new HashMap<>(){{", "}}") flags.forEach { key -> codeBuilder .add("""put("$key", new Pair<>(() -> $key, (b) -> $key = b));""") } variant.buildConfigField( "Map<String, Pair<Supplier, Consumer>>", "FEATURE_FLAG_ACCESSOR_MAP", codeBuilder.toString() ) FeatureFlagPlugin.kt (Gradle plugin, pseudocode)
  84. Implementation of Flag Modification 2/2 Make overridable flag accessible without

    reflection - Create a flag list with supplier/consumer pair public static final Map<...> FEATURE_FLAG_ACCESSOR_MAP = new HashMap<>() {{ put( "FEATURE_STICKER", ) ... BuildConfig.java (output, pseudocode)
  85. Implementation of Flag Modification 2/2 Make overridable flag accessible without

    reflection - Create a flag list with supplier/consumer pair public static final Map<...> FEATURE_FLAG_ACCESSOR_MAP = new HashMap<>() {{ put( "FEATURE_STICKER", new Pair<>( () -> FEATURE_STICKER, (b) -> FEATURE_STICKER = b) ) ... BuildConfig.java (output, pseudocode)
  86. Topics ‣ Pain of feature branches ‣ Overview of feature

    flag ‣ Flag implementation ‣ Limitations and considerations
  87. Appropriate Cases of Feature Flags ‣ Easy to apply a

    feature flag - Replace a class, layout file, or value ‣ Hard to apply a feature flag - Update API, library, or SDK version ‣ Enough to use a branch - Make a prototype or trial implementation
  88. Considerations for Pull Requests ‣ Keep each pull request small

    ‣ Think about the order/structure of pull requests eg., create a skeleton class → implement the logic
  89. Considerations for Release Testing ‣ Regression tests required for halfway

    feature ‣ Hard to guarantee the flag completeness - Might be revealed partially
  90. Summary ‣ Pain of feature branches ‣ Overview of feature

    flag ‣ Flag implementation ‣ Limitations and considerations
  91. Summary ‣ Pain of feature branches ‣ Overview of feature

    flag ‣ Flag implementation ‣ Limitations and considerations
  92. Summary ‣ Unmanageable conflicts ‣ Overview of feature flag ‣

    Flag implementation ‣ Limitations and considerations
  93. Summary ‣ Unmanageable conflicts ‣ Overview of feature flag ‣

    Flag implementation ‣ Limitations and considerations
  94. Summary ‣ Unmanageable conflicts ‣ Good for release process /

    information sharing ‣ Flag implementation ‣ Limitations and considerations
  95. Summary ‣ Unmanageable conflicts ‣ Good for release process /

    information sharing ‣ Flag implementation ‣ Limitations and considerations
  96. Summary ‣ Unmanageable conflicts ‣ Good for release process /

    information sharing ‣ Extendible Gradle plugin implementation ‣ Limitations and considerations
  97. Summary ‣ Unmanageable conflicts ‣ Good for release process /

    information sharing ‣ Extendible Gradle plugin implementation ‣ Limitations and considerations
  98. Summary ‣ Unmanageable conflicts ‣ Good for release process /

    information sharing ‣ Extendible Gradle plugin implementation ‣ Feature flags are not silver bullets
  99. References ‣ GitLab flow - https://docs.gitlab.com/ee/workflow/gitlab_flow.html ‣ Trunk based development

    - https://trunkbaseddevelopment.com/
  100. Thank you for listening