Skip to content

Commit d06aa20

Browse files
committed
fix(FeatureFlag): added a ReadWriteLock to the FeatureFlagStore, and replaced FeatureFlag constructor with of factory method
1 parent 9c38ec3 commit d06aa20

7 files changed

Lines changed: 134 additions & 59 deletions

File tree

bugsnag/src/main/java/com/bugsnag/FeatureFlag.java

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,13 @@ public class FeatureFlag {
1313
private final String name;
1414
private final String variant;
1515

16-
/**
17-
* Create a feature flag with a name and no variant.
18-
*
19-
* @param name the name of the feature flag
20-
*/
21-
public FeatureFlag(String name) {
22-
this(name, null);
23-
}
24-
2516
/**
2617
* Create a feature flag with a name and variant.
2718
*
28-
* @param name the name of the feature flag
19+
* @param name the name of the feature flag
2920
* @param variant the variant of the feature flag (can be null)
3021
*/
31-
public FeatureFlag(String name, String variant) {
22+
private FeatureFlag(String name, String variant) {
3223
if (name == null || name.isEmpty()) {
3324
throw new IllegalArgumentException("Feature flag name cannot be null or empty");
3425
}
@@ -77,4 +68,31 @@ public int hashCode() {
7768
public String toString() {
7869
return "FeatureFlag{name='" + name + "', variant='" + variant + "'}";
7970
}
71+
72+
/**
73+
* Create a feature flag with a name and no vairant.
74+
*
75+
* @param name the name of the feature flag
76+
*/
77+
public static FeatureFlag of(String name) {
78+
if (name == null || name.isEmpty()) {
79+
return null;
80+
}
81+
82+
return new FeatureFlag(name, null);
83+
}
84+
85+
/**
86+
* Create a feature flag with a name and variant.
87+
*
88+
* @param name the name of the feature flag
89+
* @param variant the variant of the feature flag (can be null)
90+
*/
91+
public static FeatureFlag of(String name, String variant) {
92+
if (name == null || name.isEmpty()) {
93+
return null;
94+
}
95+
96+
return new FeatureFlag(name, variant);
97+
}
8098
}

bugsnag/src/main/java/com/bugsnag/FeatureFlagStore.java

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import java.util.LinkedHashMap;
66
import java.util.List;
77
import java.util.Map;
8+
import java.util.concurrent.locks.ReadWriteLock;
9+
import java.util.concurrent.locks.ReentrantReadWriteLock;
810

911
/**
1012
* Internal storage for feature flags that maintains insertion order.
@@ -13,19 +15,25 @@
1315
class FeatureFlagStore {
1416
// LinkedHashMap maintains insertion order
1517
private final Map<String, String> flags = new LinkedHashMap<String, String>();
18+
private final ReadWriteLock lock = new ReentrantReadWriteLock();
1619

1720
/**
1821
* Add a feature flag with the specified name and variant.
1922
* If the name already exists, the variant will be updated without changing position.
2023
*
21-
* @param name the feature flag name
24+
* @param name the feature flag name
2225
* @param variant the feature flag variant (can be null)
2326
*/
24-
synchronized void addFeatureFlag(String name, String variant) {
27+
void addFeatureFlag(String name, String variant) {
2528
if (name == null || name.isEmpty()) {
2629
return;
2730
}
28-
flags.put(name, variant);
31+
lock.writeLock().lock();
32+
try {
33+
flags.put(name, variant);
34+
} finally {
35+
lock.writeLock().unlock();
36+
}
2937
}
3038

3139
/**
@@ -34,14 +42,20 @@ synchronized void addFeatureFlag(String name, String variant) {
3442
*
3543
* @param featureFlags the feature flags to add
3644
*/
37-
synchronized void addFeatureFlags(Collection<FeatureFlag> featureFlags) {
38-
if (featureFlags == null) {
45+
void addFeatureFlags(Collection<FeatureFlag> featureFlags) {
46+
if (featureFlags == null || featureFlags.isEmpty()) {
3947
return;
4048
}
41-
for (FeatureFlag flag : featureFlags) {
42-
if (flag != null) {
43-
addFeatureFlag(flag.getName(), flag.getVariant());
49+
50+
lock.writeLock().lock();
51+
try {
52+
for (FeatureFlag flag : featureFlags) {
53+
if (flag != null) {
54+
flags.put(flag.getName(), flag.getVariant());
55+
}
4456
}
57+
} finally {
58+
lock.writeLock().unlock();
4559
}
4660
}
4761

@@ -50,40 +64,63 @@ synchronized void addFeatureFlags(Collection<FeatureFlag> featureFlags) {
5064
*
5165
* @param name the feature flag name to remove
5266
*/
53-
synchronized void clearFeatureFlag(String name) {
67+
void clearFeatureFlag(String name) {
5468
if (name != null) {
55-
flags.remove(name);
69+
lock.writeLock().lock();
70+
try {
71+
flags.remove(name);
72+
} finally {
73+
lock.writeLock().unlock();
74+
}
5675
}
5776
}
5877

5978
/**
6079
* Remove all feature flags.
6180
*/
62-
synchronized void clearFeatureFlags() {
63-
flags.clear();
81+
void clearFeatureFlags() {
82+
lock.writeLock().lock();
83+
try {
84+
flags.clear();
85+
} finally {
86+
lock.writeLock().unlock();
87+
}
6488
}
6589

6690
/**
6791
* Get a list of all feature flags in insertion order.
6892
*
6993
* @return an unmodifiable list of feature flags
7094
*/
71-
synchronized List<FeatureFlag> toList() {
72-
List<FeatureFlag> result = new ArrayList<FeatureFlag>(flags.size());
73-
for (Map.Entry<String, String> entry : flags.entrySet()) {
74-
result.add(new FeatureFlag(entry.getKey(), entry.getValue()));
95+
List<FeatureFlag> toList() {
96+
lock.readLock().lock();
97+
try {
98+
List<FeatureFlag> result = new ArrayList<>(flags.size());
99+
for (Map.Entry<String, String> entry : flags.entrySet()) {
100+
FeatureFlag flag = FeatureFlag.of(entry.getKey(), entry.getValue());
101+
if (flag != null) {
102+
result.add(flag);
103+
}
104+
}
105+
return result;
106+
} finally {
107+
lock.readLock().unlock();
75108
}
76-
return result;
77109
}
78110

79111
/**
80112
* Create a copy of this store with all the same flags.
81113
*
82114
* @return a new FeatureFlagStore with the same flags
83115
*/
84-
synchronized FeatureFlagStore copy() {
116+
FeatureFlagStore copy() {
85117
FeatureFlagStore copy = new FeatureFlagStore();
86-
copy.flags.putAll(this.flags);
118+
lock.readLock().lock();
119+
try {
120+
copy.flags.putAll(this.flags);
121+
} finally {
122+
lock.readLock().unlock();
123+
}
87124
return copy;
88125
}
89126

@@ -94,14 +131,29 @@ synchronized FeatureFlagStore copy() {
94131
*
95132
* @param other the other store to merge from
96133
*/
97-
synchronized void merge(FeatureFlagStore other) {
98-
if (other == null) {
134+
void merge(FeatureFlagStore other) {
135+
if (other == null || other == this) {
99136
return;
100137
}
101-
synchronized (other) {
102-
for (Map.Entry<String, String> entry : other.flags.entrySet()) {
103-
flags.put(entry.getKey(), entry.getValue());
138+
139+
// Warning: this *looks* like a classic deadlock pattern, but because this method is only ever called
140+
// with isolated copies of FeatureFlagStore, the locks will never actually be contended.
141+
// If this method were to be called with two live stores, then it would be possible for a deadlock to occur.
142+
other.lock.readLock().lock();
143+
try {
144+
// we don't use other.size() because it grabs a lock.readLock() again
145+
if (other.flags.isEmpty()) {
146+
return;
104147
}
148+
149+
lock.writeLock().lock();
150+
try {
151+
flags.putAll(other.flags);
152+
} finally {
153+
lock.writeLock().unlock();
154+
}
155+
} finally {
156+
other.lock.readLock().unlock();
105157
}
106158
}
107159

@@ -110,7 +162,12 @@ synchronized void merge(FeatureFlagStore other) {
110162
*
111163
* @return the number of feature flags
112164
*/
113-
synchronized int size() {
114-
return flags.size();
165+
int size() {
166+
lock.readLock().lock();
167+
try {
168+
return flags.size();
169+
} finally {
170+
lock.readLock().unlock();
171+
}
115172
}
116173
}

bugsnag/src/test/java/com/bugsnag/BugsnagFeatureFlagTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public void testAddFeatureFlagWithoutVariant() {
5353
@Test
5454
public void testAddFeatureFlags() {
5555
List<FeatureFlag> flagsToAdd = new ArrayList<FeatureFlag>();
56-
flagsToAdd.add(new FeatureFlag("flag1", "variant-a"));
57-
flagsToAdd.add(new FeatureFlag("flag2", "variant-b"));
56+
flagsToAdd.add(FeatureFlag.of("flag1", "variant-a"));
57+
flagsToAdd.add(FeatureFlag.of("flag2", "variant-b"));
5858

5959
bugsnag.addFeatureFlags(flagsToAdd);
6060

bugsnag/src/test/java/com/bugsnag/ConfigurationFeatureFlagTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ public void testAddFeatureFlagWithoutVariant() {
4848
@Test
4949
public void testAddFeatureFlags() {
5050
List<FeatureFlag> flagsToAdd = new ArrayList<FeatureFlag>();
51-
flagsToAdd.add(new FeatureFlag("flag1", "variant-a"));
52-
flagsToAdd.add(new FeatureFlag("flag2", "variant-b"));
51+
flagsToAdd.add(FeatureFlag.of("flag1", "variant-a"));
52+
flagsToAdd.add(FeatureFlag.of("flag2", "variant-b"));
5353

5454
config.addFeatureFlags(flagsToAdd);
5555

bugsnag/src/test/java/com/bugsnag/FeatureFlagStoreTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ public void testAddFeatureFlagUpdateDoesNotChangeOrder() {
7878
@Test
7979
public void testAddFeatureFlags() {
8080
List<FeatureFlag> flagsToAdd = new ArrayList<FeatureFlag>();
81-
flagsToAdd.add(new FeatureFlag("flag1", "variant-a"));
82-
flagsToAdd.add(new FeatureFlag("flag2", "variant-b"));
81+
flagsToAdd.add(FeatureFlag.of("flag1", "variant-a"));
82+
flagsToAdd.add(FeatureFlag.of("flag2", "variant-b"));
8383

8484
store.addFeatureFlags(flagsToAdd);
8585

bugsnag/src/test/java/com/bugsnag/FeatureFlagTest.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,41 @@ public class FeatureFlagTest {
1313

1414
@Test
1515
public void testFeatureFlagWithVariant() {
16-
FeatureFlag flag = new FeatureFlag("test-flag", "variant-a");
16+
FeatureFlag flag = FeatureFlag.of("test-flag", "variant-a");
1717
assertEquals("test-flag", flag.getName());
1818
assertEquals("variant-a", flag.getVariant());
1919
}
2020

2121
@Test
2222
public void testFeatureFlagWithoutVariant() {
23-
FeatureFlag flag = new FeatureFlag("test-flag");
23+
FeatureFlag flag = FeatureFlag.of("test-flag");
2424
assertEquals("test-flag", flag.getName());
2525
assertNull(flag.getVariant());
2626
}
2727

2828
@Test
2929
public void testFeatureFlagWithNullVariant() {
30-
FeatureFlag flag = new FeatureFlag("test-flag", null);
30+
FeatureFlag flag = FeatureFlag.of("test-flag", null);
3131
assertEquals("test-flag", flag.getName());
3232
assertNull(flag.getVariant());
3333
}
3434

35-
@Test(expected = IllegalArgumentException.class)
35+
@Test
3636
public void testFeatureFlagWithNullName() {
37-
new FeatureFlag(null);
37+
assertNull(FeatureFlag.of(null));
3838
}
3939

40-
@Test(expected = IllegalArgumentException.class)
40+
@Test
4141
public void testFeatureFlagWithEmptyName() {
42-
new FeatureFlag("");
42+
assertNull(FeatureFlag.of(""));
4343
}
4444

4545
@Test
4646
public void testFeatureFlagEquals() {
47-
FeatureFlag flag1 = new FeatureFlag("test", "variant-a");
48-
FeatureFlag flag2 = new FeatureFlag("test", "variant-a");
49-
FeatureFlag flag3 = new FeatureFlag("test", "variant-b");
50-
FeatureFlag flag4 = new FeatureFlag("other", "variant-a");
47+
FeatureFlag flag1 = FeatureFlag.of("test", "variant-a");
48+
FeatureFlag flag2 = FeatureFlag.of("test", "variant-a");
49+
FeatureFlag flag3 = FeatureFlag.of("test", "variant-b");
50+
FeatureFlag flag4 = FeatureFlag.of("other", "variant-a");
5151

5252
assertEquals(flag1, flag2);
5353
assertTrue(!flag1.equals(flag3));
@@ -56,14 +56,14 @@ public void testFeatureFlagEquals() {
5656

5757
@Test
5858
public void testFeatureFlagHashCode() {
59-
FeatureFlag flag1 = new FeatureFlag("test", "variant-a");
60-
FeatureFlag flag2 = new FeatureFlag("test", "variant-a");
59+
FeatureFlag flag1 = FeatureFlag.of("test", "variant-a");
60+
FeatureFlag flag2 = FeatureFlag.of("test", "variant-a");
6161
assertEquals(flag1.hashCode(), flag2.hashCode());
6262
}
6363

6464
@Test
6565
public void testFeatureFlagToString() {
66-
FeatureFlag flag = new FeatureFlag("test-flag", "variant-a");
66+
FeatureFlag flag = FeatureFlag.of("test-flag", "variant-a");
6767
String result = flag.toString();
6868
assertTrue(result.contains("test-flag"));
6969
assertTrue(result.contains("variant-a"));

bugsnag/src/test/java/com/bugsnag/ReportFeatureFlagTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public void testAddFeatureFlagWithoutVariant() {
5353
@Test
5454
public void testAddFeatureFlags() {
5555
List<FeatureFlag> flagsToAdd = new ArrayList<FeatureFlag>();
56-
flagsToAdd.add(new FeatureFlag("flag1", "variant-a"));
57-
flagsToAdd.add(new FeatureFlag("flag2", "variant-b"));
56+
flagsToAdd.add(FeatureFlag.of("flag1", "variant-a"));
57+
flagsToAdd.add(FeatureFlag.of("flag2", "variant-b"));
5858

5959
Report report = bugsnag.buildReport(new RuntimeException("Test"));
6060
report.addFeatureFlags(flagsToAdd);

0 commit comments

Comments
 (0)