Semaphore 0.3 and 1.0

Semaphore is an open source library I wrote that helps add feature flags to CFML projects.

Today I am releasing Semaphore version 0.3, which includes no functional changes, but serves as a ⚠️ deprecation warning ⚠️ for breaking changes coming in v1.0, soon[1].

My team has been using Semaphore in production for almost a year now, and it's been working great with one annoying exception. The way the rules engine is implemented means that in order to accomplish a true combination of "AND" and "OR" rules for a single feature, we ended up hoisting the OR implementation out a layer into what could be considered "userland" code. There's currently no way to do it inside of semaphore.

Long story short, we're tired of not having a good way to mix AND and OR support together, so I'm fixing that flaw; and it involves some breaking changes.

Unless you're using Semaphore and you need to plan for the upcoming data structure changes, you can probably stop reading here. The rest is very technical and will be meaningless unless you care about implementing this specific project, or I guess if you have an interest in rules engines.

Either way, you've been warned. :)


The ultra-short version

Specifying rules in v0.2:

{
matchRules: 'ALL', //ALL = AND, ANY = OR
rules: [RULE, RULE, RULE]
}

Specifying rules in v1.0+:

{
rules: [
[RULE, /* and */ RULE, /* and */ RULE],
/* or */
[RULE, /* and */ RULE, /* and */ RULE]
];
}

The full explanation

Consider these rules: Feature X should be enabled if:

  1. env == development; OR
  2. (env == QA AND customer == Disney); OR
  3. (env == production AND customer in Disney, Pop Tarts)

Semaphore v0.2 allows you to create a flat list of rules, and a single combination expression (AND vs. OR). Because of this, you can't have different subsets of rules for different environments, for example. You can have a flag that will be on when env=development; and you can have a flag that will be on when env=production AND customer in list "Disney, Pop Tarts"... But you can't use both AND and OR in a single flag.

So, what we did on our team was wrap semaphore in a featureFlagService which allowed us to check multiple flags at once, and return true if any of those flags evaluated to true. This allowed us to create separate flags, i.e. some-feature-dev, some-feature-qa, and some-feature-prod, each of them implementing only 1 of the numbered sets of rules above, and then we would check all of those flags in our code:

if (flagEnabled('some-feature-dev,some-feature-qa,some-feature-prod')) {
newImplementation();
} else {
oldImplementation();
}

This works fine, but it's annoying, because it requires us to manage 3 feature flags for every feature because we usually have 3 possible OR-cases. If you have more OR's, then you'd need even more flags!

Fixing this requires making a breaking change to the DSL for specifying flag rules. If you have some sort of GUI for creating and managing flags, it will need to be updated to read and output these changes to the underlying data.

Currently, a flag consists of:

'flag-id': {
name: string,
description: string,
active: boolean,
baseState: boolean,
matchRules: ANY | ALL,
rules: [
RULE,
RULE...
]
}

and RULEs consist of:

{
type: filter | % | nobody | everybody,
attribute: string,
operator: == | != | < | <= | > | >= | in | has,
comparator: boolean | numeric | string | string[]
}

In order to support combinations of AND/OR, the flag structure is changing to the following:

'flag-id': {
name: string,
description: string,
active: boolean,
baseState: boolean,
rules: [
[ RULE, RULE, RULE, ... ],
[ RULE, RULE, ... ],
]
}

Note that the matchRules property is gone. It will now always behave as an OR against the top-level arrays, and an AND for the rules inside each inner array.

Hopefully that makes sense. Basically:

rules: [
[RULE, /* and */ RULE, /* and */ RULE],
/* or */
[RULE, /* and */ RULE, /* and */ RULE]
/* or... */
];

This exact same structure will be required in ALL cases. That includes scenarios where you have just two rules and you want them to be evaluated using an OR:

rules: [
[this],
/* or */
[that]
];

... Or even just a single rule:

rules: [[other]];

This approach is still a little bit naïve, but what it lacks in cleverness it repays in simplicity. So yes, it's true that you won't be able to have rules with deeply nested AND/OR expressions, but if you're willing to suffer some (probably minor) duplication, it should solve for every possible need.

I expect that this change should greatly improve my teams happiness using Semaphore[2], and hopefully yours too.


  1. Tentatively targeting May 19th; exactly 1 year after Semaphore's introduction. ↩︎

  2. To be fair, we're already quite happy with it! ↩︎

Webmentions

It's like comments, but you do it on Twitter.

1 Like

James Moberg

6 Replies

Gavin Pickin Gavin Pickin
In semver - isn’t a minor release treated as a major (ie breaking changes and no auto update) if you are under 1.0.0 still?
Steve Bryant Steve Bryant
I'm not using Semaphore, but I wish I'd seen it when it came out and I would be. We developed something internally (more specific) at the time. I'll definitely need to think about switching to this at some point. Cool stuff!
I've seen people treat it that way, but that's not outlined in the semver 2.0.0 spec. (semver.org/#semantic-vers…) It does say anything can change at any time for any change in 0.x. So: 🤷🏻‍♂️
It also says if you're using it in prod you should be at 1.0+. I figure it's been stable for almost a year now so it's time for 1.0. Bumping to 0.3 seemed like a better choice for the deprecation than 0.2.1.
Gavin Pickin Gavin Pickin
Ok. Cool… it throws a lot of people off, myself included
Add your comment: Tweet about this article.

Webmentions via webmention.io.

Discuss on TwitterEdit on GitHubContributions