2525import lombok .NonNull ;
2626
2727import com .google .gson .Gson ;
28+ import com .microsoft .identity .common .java .flighting .CommonFlight ;
29+ import com .microsoft .identity .common .java .flighting .CommonFlightsManager ;
2830import com .microsoft .identity .common .java .interfaces .IPlatformComponents ;
2931import com .microsoft .identity .common .java .interfaces .INameValueStorage ;
3032import com .microsoft .identity .common .java .logging .Logger ;
3133import com .microsoft .identity .common .java .util .StringUtil ;
3234
3335import java .util .HashSet ;
3436import java .util .List ;
37+ import java .util .Map ;
3538import java .util .Set ;
3639import java .util .concurrent .Callable ;
40+ import java .util .concurrent .ConcurrentHashMap ;
3741import java .util .concurrent .TimeUnit ;
42+ import java .util .concurrent .locks .ReentrantReadWriteLock ;
3843
3944/**
4045 * A simple metadata store definition that uses INameValueStorage to persist, read,
@@ -59,6 +64,12 @@ public abstract class NameValueStorageFileManagerSimpleCacheImpl<T> implements I
5964 private final boolean mForceReinsertionOfDuplicates ;
6065 private final Gson mGson = new Gson ();
6166
67+ // Lock per storage name to prevent corruption when multiple instances access the same storage file
68+ private static final Map <String , ReentrantReadWriteLock > STORAGE_LOCKS = new ConcurrentHashMap <>();
69+ private final ReentrantReadWriteLock metadataCacheLock ;
70+
71+ private final boolean useLocks ;
72+
6273 /**
6374 * Constructs a new NameValueStorageFileManagerSimpleCacheImpl. Convenience class for persisting
6475 * lists of arbitrarily-typed data. Duplicate reinsertion is disabled (backcompat) by default.
@@ -93,6 +104,9 @@ public NameValueStorageFileManagerSimpleCacheImpl(@NonNull final IPlatformCompon
93104 mStorage = components .getStorageSupplier ().getUnencryptedNameValueStore (name , String .class );
94105 mKeySingleEntry = singleKey ;
95106 mForceReinsertionOfDuplicates = forceReinsertionOfDuplicates ;
107+ // Get or create a lock for this specific storage file
108+ metadataCacheLock = STORAGE_LOCKS .computeIfAbsent (name , k -> new ReentrantReadWriteLock ());
109+ useLocks = CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .USE_LOCKS_IN_NAME_VALUE_STORAGE );
96110 }
97111
98112 private interface NamedRunnable <V > extends Callable <V > {
@@ -121,6 +135,9 @@ private <V> V execWithTiming(@NonNull final NamedRunnable<V> runnable) {
121135
122136 @ Override
123137 public boolean insert (final T t ) {
138+ if (useLocks ) {
139+ return insertWithLocks (t );
140+ }
124141 return execWithTiming (new NamedRunnable <Boolean >() {
125142 @ Override
126143 public String getName () {
@@ -129,7 +146,7 @@ public String getName() {
129146
130147 @ Override
131148 public Boolean call () {
132- final Set <T > allMetadata = new HashSet <>(getAll ());
149+ final Set <T > allMetadata = new HashSet <>(getAllInternal ());
133150
134151 if (mForceReinsertionOfDuplicates ) {
135152 // This is a bit of workaround for Set's default behavior
@@ -147,27 +164,90 @@ public Boolean call() {
147164 });
148165 }
149166
167+ private boolean insertWithLocks (final T t ) {
168+ metadataCacheLock .writeLock ().lock ();
169+ try {
170+ return execWithTiming (new NamedRunnable <Boolean >() {
171+ @ Override
172+ public String getName () {
173+ return "insert" ;
174+ }
175+
176+ @ Override
177+ public Boolean call () {
178+ final Set <T > allMetadata = new HashSet <>(getAllInternal ());
179+
180+ if (mForceReinsertionOfDuplicates ) {
181+ // This is a bit of workaround for Set's default behavior
182+ // where items already within the Set are not replaced if they are
183+ // inserted but already exist.
184+ // This makes it behave more like a Map.
185+ allMetadata .remove (t );
186+ }
187+
188+ allMetadata .add (t );
189+ final String json = mGson .toJson (allMetadata );
190+ mStorage .put (mKeySingleEntry , json );
191+ return true ;
192+ }
193+ });
194+ } finally {
195+ metadataCacheLock .writeLock ().unlock ();
196+ }
197+ }
198+
150199 @ Override
151200 public boolean remove (final T t ) {
152- return execWithTiming (new NamedRunnable <Boolean >() {
153- @ Override
154- public String getName () {
155- return "remove" ;
156- }
201+ metadataCacheLock .writeLock ().lock ();
202+ try {
203+ return execWithTiming (new NamedRunnable <Boolean >() {
204+ @ Override
205+ public String getName () {
206+ return "remove" ;
207+ }
208+
209+ @ Override
210+ public Boolean call () {
211+ final Set <T > allMetadata = new HashSet <>(getAllInternal ());
212+ allMetadata .remove (t );
213+ final String json = mGson .toJson (allMetadata );
214+ mStorage .put (mKeySingleEntry , json );
215+ return true ;
216+ }
217+ });
218+ } finally {
219+ metadataCacheLock .writeLock ().unlock ();
220+ }
157221
158- @ Override
159- public Boolean call () {
160- final Set <T > allMetadata = new HashSet <>(getAll ());
161- allMetadata .remove (t );
162- final String json = mGson .toJson (allMetadata );
163- mStorage .put (mKeySingleEntry , json );
164- return true ;
165- }
166- });
167222 }
168223
169224 @ Override
170225 public List <T > getAll () {
226+ metadataCacheLock .readLock ().lock ();
227+ try {
228+ return getAllInternal ();
229+ } finally {
230+ metadataCacheLock .readLock ().unlock ();
231+ }
232+ }
233+
234+ private List <T > getAllWithLocks () {
235+ metadataCacheLock .readLock ().lock ();
236+ try {
237+ return getAllInternal ();
238+ } finally {
239+ metadataCacheLock .readLock ().unlock ();
240+ }
241+ }
242+
243+ /**
244+ * Internal helper method to retrieve all items from storage without acquiring a lock.
245+ * This method should only be called when a lock (read or write) is already held by the caller.
246+ * For external access, use {@link #getAll()} which properly acquires a read lock.
247+ *
248+ * @return List of all items in the cache
249+ */
250+ private List <T > getAllInternal () {
171251 return execWithTiming (new NamedRunnable <List <T >>() {
172252 @ Override
173253 public String getName () {
@@ -191,6 +271,9 @@ public List<T> call() {
191271
192272 @ Override
193273 public boolean clear () {
274+ if (useLocks ) {
275+ return clearWithLocks ();
276+ }
194277 return execWithTiming (new NamedRunnable <Boolean >() {
195278 @ Override
196279 public String getName () {
@@ -204,4 +287,24 @@ public Boolean call() {
204287 }
205288 });
206289 }
290+
291+ private boolean clearWithLocks () {
292+ metadataCacheLock .writeLock ().lock ();
293+ try {
294+ return execWithTiming (new NamedRunnable <Boolean >() {
295+ @ Override
296+ public String getName () {
297+ return "clear" ;
298+ }
299+
300+ @ Override
301+ public Boolean call () {
302+ mStorage .clear ();
303+ return true ;
304+ }
305+ });
306+ } finally {
307+ metadataCacheLock .writeLock ().unlock ();
308+ }
309+ }
207310}
0 commit comments