1 /*
2  * Copyright (c) Facebook, Inc. and its affiliates.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include <chrono>
18 #include <thread>
19 #include <vector>
20 
21 #include <folly/Memory.h>
22 #include <folly/executors/ManualExecutor.h>
23 #include <folly/futures/Future.h>
24 #include <folly/portability/GMock.h>
25 #include <folly/portability/GTest.h>
26 #include <folly/synchronization/Baton.h>
27 #include <wangle/client/persistence/LRUPersistentCache.h>
28 #include <wangle/client/persistence/SharedMutexCacheLockGuard.h>
29 
30 using namespace folly;
31 using namespace std;
32 using namespace testing;
33 using namespace wangle;
34 
35 using TestPersistenceLayer = CachePersistence;
36 
37 using MutexTypes = ::testing::Types<std::mutex, folly::SharedMutex>;
38 TYPED_TEST_CASE(LRUPersistentCacheTest, MutexTypes);
39 
40 template <typename T>
createCache(size_t capacity,uint32_t syncMillis,std::unique_ptr<TestPersistenceLayer> persistence=nullptr,bool loadPersistenceInline=true)41 static shared_ptr<LRUPersistentCache<string, string, T>> createCache(
42     size_t capacity,
43     uint32_t syncMillis,
44     std::unique_ptr<TestPersistenceLayer> persistence = nullptr,
45     bool loadPersistenceInline = true) {
46   using TestCache = LRUPersistentCache<string, string, T>;
47   return std::make_shared<TestCache>(
48       PersistentCacheConfig::Builder()
49           .setCapacity(capacity)
50           .setSyncInterval(std::chrono::milliseconds(syncMillis))
51           .setSyncRetries(3)
52           .setInlinePersistenceLoading(loadPersistenceInline)
53           .build(),
54       std::move(persistence));
55 }
56 
57 template <typename T>
58 static shared_ptr<LRUPersistentCache<string, string, T>>
createCacheWithExecutor(std::shared_ptr<folly::Executor> executor,std::unique_ptr<TestPersistenceLayer> persistence,std::chrono::milliseconds syncInterval,int retryLimit,bool loadPersistenceInline=true)59 createCacheWithExecutor(
60     std::shared_ptr<folly::Executor> executor,
61     std::unique_ptr<TestPersistenceLayer> persistence,
62     std::chrono::milliseconds syncInterval,
63     int retryLimit,
64     bool loadPersistenceInline = true) {
65   return std::make_shared<LRUPersistentCache<string, string, T>>(
66       PersistentCacheConfig::Builder()
67           .setCapacity(10)
68           .setExecutor(std::move(executor))
69           .setSyncInterval(syncInterval)
70           .setSyncRetries(retryLimit)
71           .setInlinePersistenceLoading(loadPersistenceInline)
72           .build(),
73       std::move(persistence));
74 }
75 
76 class MockPersistenceLayer : public TestPersistenceLayer {
77  public:
~MockPersistenceLayer()78   ~MockPersistenceLayer() override {
79     LOG(ERROR) << "ok.";
80   }
persist(const dynamic & obj)81   bool persist(const dynamic& obj) noexcept override {
82     return persist_(obj);
83   }
load()84   folly::Optional<dynamic> load() noexcept override {
85     return load_();
86   }
getLastPersistedVersionConcrete() const87   CacheDataVersion getLastPersistedVersionConcrete() const {
88     return TestPersistenceLayer::getLastPersistedVersion();
89   }
setPersistedVersionConcrete(CacheDataVersion version)90   void setPersistedVersionConcrete(CacheDataVersion version) {
91     TestPersistenceLayer::setPersistedVersion(version);
92   }
93   MOCK_METHOD0(clear, void());
94   MOCK_METHOD1(persist_, bool(const dynamic&));
95   MOCK_METHOD0(load_, folly::Optional<dynamic>());
96   MOCK_CONST_METHOD0(getLastPersistedVersion, CacheDataVersion());
97   GMOCK_METHOD1_(, noexcept, , setPersistedVersion, void(CacheDataVersion));
98 };
99 
100 template <typename MutexT>
101 class LRUPersistentCacheTest : public Test {
102  protected:
SetUp()103   void SetUp() override {
104     persistence = make_unique<MockPersistenceLayer>();
105     ON_CALL(*persistence, getLastPersistedVersion())
106         .WillByDefault(Invoke(
107             persistence.get(),
108             &MockPersistenceLayer::getLastPersistedVersionConcrete));
109     ON_CALL(*persistence, setPersistedVersion(_))
110         .WillByDefault(Invoke(
111             persistence.get(),
112             &MockPersistenceLayer::setPersistedVersionConcrete));
113     manualExecutor = std::make_shared<folly::ManualExecutor>();
114     inlineExecutor = std::make_shared<folly::InlineExecutor>();
115   }
116 
117   unique_ptr<MockPersistenceLayer> persistence;
118   std::shared_ptr<folly::ManualExecutor> manualExecutor;
119   std::shared_ptr<folly::InlineExecutor> inlineExecutor;
120 };
121 
TYPED_TEST(LRUPersistentCacheTest,NullPersistence)122 TYPED_TEST(LRUPersistentCacheTest, NullPersistence) {
123   // make sure things sync even without a persistence layer
124   auto cache = createCache<TypeParam>(10, 1, nullptr);
125   cache->init();
126   cache->put("k0", "v0");
127   makeFuture()
128       .delayed(std::chrono::milliseconds(20))
129       .thenValue([cache](auto&&) {
130         auto val = cache->get("k0");
131         EXPECT_TRUE(val);
132         EXPECT_EQ(*val, "v0");
133         EXPECT_FALSE(cache->hasPendingUpdates());
134       });
135 }
136 
137 MATCHER_P(DynSize, n, "") {
138   return size_t(n) == arg.size();
139 }
140 
TYPED_TEST(LRUPersistentCacheTest,SettingPersistenceFromCtor)141 TYPED_TEST(LRUPersistentCacheTest, SettingPersistenceFromCtor) {
142   InSequence seq;
143   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
144   auto rawPersistence = this->persistence.get();
145   EXPECT_CALL(*rawPersistence, load_()).Times(1).WillOnce(Return(data));
146   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
147       .Times(1)
148       .WillOnce(Return(true));
149   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
150   cache->init();
151   cache->put("k0", "v0");
152 }
153 
TYPED_TEST(LRUPersistentCacheTest,SyncOnDestroy)154 TYPED_TEST(LRUPersistentCacheTest, SyncOnDestroy) {
155   auto persistence = this->persistence.get();
156   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
157   cache->init();
158   cache->put("k0", "v0");
159   EXPECT_CALL(*persistence, persist_(_)).Times(1).WillOnce(Return(true));
160   cache.reset();
161 }
162 
TYPED_TEST(LRUPersistentCacheTest,SyncOnDestroyWithExecutor)163 TYPED_TEST(LRUPersistentCacheTest, SyncOnDestroyWithExecutor) {
164   auto persistence = this->persistence.get();
165   auto cache = createCacheWithExecutor<TypeParam>(
166       this->manualExecutor,
167       std::move(this->persistence),
168       std::chrono::milliseconds::zero(),
169       1);
170   cache->init();
171   cache->put("k0", "v0");
172   EXPECT_CALL(*persistence, persist_(_))
173       .Times(AtLeast(1))
174       .WillRepeatedly(Return(true));
175   cache.reset();
176 }
177 
TYPED_TEST(LRUPersistentCacheTest,PersistNotCalled)178 TYPED_TEST(LRUPersistentCacheTest, PersistNotCalled) {
179   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
180   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
181   EXPECT_CALL(*this->persistence, persist_(_)).Times(0).WillOnce(Return(false));
182   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
183   cache->init();
184   EXPECT_EQ(cache->size(), 1);
185 }
186 
TYPED_TEST(LRUPersistentCacheTest,PersistentSetBeforeSyncer)187 TYPED_TEST(LRUPersistentCacheTest, PersistentSetBeforeSyncer) {
188   EXPECT_CALL(*this->persistence, getLastPersistedVersion())
189       .Times(AtLeast(1))
190       .WillRepeatedly(Invoke(
191           this->persistence.get(),
192           &MockPersistenceLayer::getLastPersistedVersionConcrete));
193   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
194   cache->init();
195 }
196 
TYPED_TEST(LRUPersistentCacheTest,ClearKeepPersist)197 TYPED_TEST(LRUPersistentCacheTest, ClearKeepPersist) {
198   EXPECT_CALL(*this->persistence, clear()).Times(0);
199   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
200   cache->init();
201   cache->clear();
202 }
203 
TYPED_TEST(LRUPersistentCacheTest,ClearDontKeepPersist)204 TYPED_TEST(LRUPersistentCacheTest, ClearDontKeepPersist) {
205   EXPECT_CALL(*this->persistence, clear()).Times(1);
206   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
207   cache->init();
208   cache->clear(true);
209 }
210 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheDeallocBeforeAdd)211 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheDeallocBeforeAdd) {
212   auto cache = createCacheWithExecutor<TypeParam>(
213       this->manualExecutor,
214       std::move(this->persistence),
215       std::chrono::milliseconds::zero(),
216       1);
217   cache.reset();
218   // Nothing should happen here
219   this->manualExecutor->drain();
220 }
221 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheRunTask)222 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheRunTask) {
223   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
224   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
225   auto rawPersistence = this->persistence.get();
226   auto cache = createCacheWithExecutor<TypeParam>(
227       this->manualExecutor,
228       std::move(this->persistence),
229       std::chrono::milliseconds::zero(),
230       1);
231   cache->init();
232   this->manualExecutor->run();
233   cache->put("k0", "v0");
234   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
235       .Times(1)
236       .WillOnce(Invoke(
237           rawPersistence,
238           &MockPersistenceLayer::getLastPersistedVersionConcrete));
239   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
240       .Times(1)
241       .WillOnce(Return(true));
242   this->manualExecutor->run();
243 
244   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
245       .Times(1)
246       .WillOnce(Invoke(
247           rawPersistence,
248           &MockPersistenceLayer::getLastPersistedVersionConcrete));
249   cache.reset();
250 }
251 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheRunTaskInline)252 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheRunTaskInline) {
253   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
254   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
255   auto rawPersistence = this->persistence.get();
256   auto cache = createCacheWithExecutor<TypeParam>(
257       this->inlineExecutor,
258       std::move(this->persistence),
259       std::chrono::milliseconds::zero(),
260       1);
261   cache->init();
262   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
263       .Times(1)
264       .WillOnce(Invoke(
265           rawPersistence,
266           &MockPersistenceLayer::getLastPersistedVersionConcrete));
267   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
268       .Times(1)
269       .WillOnce(Return(true));
270   cache->put("k0", "v0");
271 
272   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
273       .Times(1)
274       .WillOnce(Invoke(
275           rawPersistence,
276           &MockPersistenceLayer::getLastPersistedVersionConcrete));
277   EXPECT_CALL(*rawPersistence, persist_(DynSize(3)))
278       .Times(1)
279       .WillOnce(Return(true));
280   cache->put("k2", "v2");
281 
282   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
283       .Times(1)
284       .WillOnce(Invoke(
285           rawPersistence,
286           &MockPersistenceLayer::getLastPersistedVersionConcrete));
287   cache.reset();
288 }
289 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheRetries)290 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheRetries) {
291   EXPECT_CALL(*this->persistence, load_())
292       .Times(1)
293       .WillOnce(Return(dynamic::array()));
294   auto rawPersistence = this->persistence.get();
295   auto cache = createCacheWithExecutor<TypeParam>(
296       this->manualExecutor,
297       std::move(this->persistence),
298       std::chrono::milliseconds::zero(),
299       2);
300   cache->init();
301   this->manualExecutor->run();
302   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
303       .WillRepeatedly(Invoke(
304           rawPersistence,
305           &MockPersistenceLayer::getLastPersistedVersionConcrete));
306 
307   cache->put("k0", "v0");
308   EXPECT_CALL(*rawPersistence, persist_(DynSize(1)))
309       .Times(1)
310       .WillOnce(Return(false));
311   this->manualExecutor->run();
312 
313   cache->put("k1", "v1");
314   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
315       .Times(1)
316       .WillOnce(Return(false));
317   // reached retry limit, so we will set a version anyway
318   EXPECT_CALL(*rawPersistence, setPersistedVersion(_))
319       .Times(1)
320       .WillOnce(Invoke(
321           rawPersistence, &MockPersistenceLayer::setPersistedVersionConcrete));
322   this->manualExecutor->run();
323 
324   cache.reset();
325 }
326 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheSchduledAndDealloc)327 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheSchduledAndDealloc) {
328   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
329   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
330   auto cache = createCacheWithExecutor<TypeParam>(
331       this->manualExecutor,
332       std::move(this->persistence),
333       std::chrono::milliseconds::zero(),
334       1);
335   cache->init();
336   this->manualExecutor->run();
337   cache->put("k0", "v0");
338   cache->put("k2", "v2");
339 
340   // Kill cache first then try to run scheduled tasks. Nothing will run and no
341   // one should crash.
342   cache.reset();
343   this->manualExecutor->drain();
344 }
345 
TYPED_TEST(LRUPersistentCacheTest,ExecutorCacheScheduleInterval)346 TYPED_TEST(LRUPersistentCacheTest, ExecutorCacheScheduleInterval) {
347   EXPECT_CALL(*this->persistence, load_())
348       .Times(1)
349       .WillOnce(Return(dynamic::array()));
350   auto rawPersistence = this->persistence.get();
351   auto cache = createCacheWithExecutor<TypeParam>(
352       this->manualExecutor,
353       std::move(this->persistence),
354       std::chrono::milliseconds(60 * 60 * 1000),
355       1);
356   cache->init();
357   this->manualExecutor->run();
358   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
359       .WillRepeatedly(Invoke(
360           rawPersistence,
361           &MockPersistenceLayer::getLastPersistedVersionConcrete));
362 
363   cache->put("k0", "v0");
364   EXPECT_CALL(*rawPersistence, persist_(DynSize(1)))
365       .Times(1)
366       .WillOnce(Return(false));
367   this->manualExecutor->run();
368 
369   // The following put won't trigger a run due to the interval
370   EXPECT_CALL(*rawPersistence, persist_(DynSize(2))).Times(0);
371   EXPECT_CALL(*rawPersistence, setPersistedVersion(_)).Times(0);
372   cache->put("k1", "v1");
373   this->manualExecutor->run();
374 
375   // But we will sync again upon destroy
376   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
377       .Times(1)
378       .WillOnce(Return(true));
379   cache.reset();
380   // Nothing more should happen after this
381   this->manualExecutor->drain();
382 }
383 
TYPED_TEST(LRUPersistentCacheTest,InitCache)384 TYPED_TEST(LRUPersistentCacheTest, InitCache) {
385   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
386   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
387   auto cache = createCacheWithExecutor<TypeParam>(
388       this->inlineExecutor,
389       std::move(this->persistence),
390       std::chrono::milliseconds::zero(),
391       1,
392       false);
393   cache->init();
394   EXPECT_FALSE(cache->hasPendingUpdates());
395 }
396 
TYPED_TEST(LRUPersistentCacheTest,BlockingAccessCanContinueWithExecutor)397 TYPED_TEST(LRUPersistentCacheTest, BlockingAccessCanContinueWithExecutor) {
398   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
399   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
400   auto cache = createCacheWithExecutor<TypeParam>(
401       this->manualExecutor,
402       std::move(this->persistence),
403       std::chrono::milliseconds::zero(),
404       1,
405       false);
406   cache->init();
407   std::string value1, value2;
408   std::thread willBeBlocked([&]() { value1 = cache->get("k1").value(); });
409   // Without running the executor to finish setPersistenceHelper in init,
410   // willbeBlocked will be blocked.
411   this->manualExecutor->run();
412   willBeBlocked.join();
413   EXPECT_EQ("v1", value1);
414 
415   std::thread wontBeBlocked([&]() { value2 = cache->get("k1").value(); });
416   wontBeBlocked.join();
417   EXPECT_EQ("v1", value2);
418 }
419 
TYPED_TEST(LRUPersistentCacheTest,BlockingAccessCanContinueWithThread)420 TYPED_TEST(LRUPersistentCacheTest, BlockingAccessCanContinueWithThread) {
421   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
422   EXPECT_CALL(*this->persistence, load_()).Times(1).WillOnce(Return(data));
423   auto cache =
424       createCache<TypeParam>(10, 10, std::move(this->persistence), false);
425   cache->init();
426   std::string value1;
427   std::thread appThread([&]() { value1 = cache->get("k1").value(); });
428   appThread.join();
429   EXPECT_EQ("v1", value1);
430 }
431 
TYPED_TEST(LRUPersistentCacheTest,PersistenceOnlyLoadedOnceFromCtorWithExecutor)432 TYPED_TEST(
433     LRUPersistentCacheTest,
434     PersistenceOnlyLoadedOnceFromCtorWithExecutor) {
435   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
436   auto persistence = this->persistence.get();
437   EXPECT_CALL(*persistence, load_()).Times(1).WillOnce(Return(data));
438   auto cache = createCacheWithExecutor<TypeParam>(
439       this->manualExecutor,
440       std::move(this->persistence),
441       std::chrono::milliseconds::zero(),
442       1);
443   cache->init();
444 }
445 
TYPED_TEST(LRUPersistentCacheTest,PersistenceOnlyLoadedOnceFromCtorWithSyncThread)446 TYPED_TEST(
447     LRUPersistentCacheTest,
448     PersistenceOnlyLoadedOnceFromCtorWithSyncThread) {
449   folly::dynamic data = dynamic::array(dynamic::array("k1", "v1"));
450   auto persistence = this->persistence.get();
451   EXPECT_CALL(*persistence, load_()).Times(1).WillOnce(Return(data));
452   auto cache = createCache<TypeParam>(10, 1, std::move(this->persistence));
453   cache->init();
454 }
455 
TYPED_TEST(LRUPersistentCacheTest,DestroyWithoutInitThreadMode)456 TYPED_TEST(LRUPersistentCacheTest, DestroyWithoutInitThreadMode) {
457   auto cache = createCache<TypeParam>(10, 10, std::move(this->persistence));
458   cache.reset();
459 }
460 
TYPED_TEST(LRUPersistentCacheTest,DestroyWithoutInitExecutorMode)461 TYPED_TEST(LRUPersistentCacheTest, DestroyWithoutInitExecutorMode) {
462   auto cache = createCacheWithExecutor<TypeParam>(
463       this->inlineExecutor,
464       std::move(this->persistence),
465       std::chrono::milliseconds::zero(),
466       1);
467   cache.reset();
468 }
469 
TYPED_TEST(LRUPersistentCacheTest,EmptyPersistenceMatchesEmptyCache)470 TYPED_TEST(LRUPersistentCacheTest, EmptyPersistenceMatchesEmptyCache) {
471   auto persistence = this->persistence.get();
472   EXPECT_CALL(*persistence, load_()).Times(1).WillOnce(Return(folly::none));
473   auto cache = createCacheWithExecutor<TypeParam>(
474       this->manualExecutor,
475       std::move(this->persistence),
476       std::chrono::milliseconds::zero(),
477       1);
478   cache->init();
479   EXPECT_FALSE(cache->hasPendingUpdates());
480   EXPECT_EQ(
481       kDefaultInitCacheDataVersion, persistence->getLastPersistedVersion());
482   this->manualExecutor->drain();
483 
484   cache->put("k0", "v0");
485   EXPECT_TRUE(cache->hasPendingUpdates());
486 }
487 
TYPED_TEST(LRUPersistentCacheTest,ZeroSyncIntervalSyncsImmediately)488 TYPED_TEST(LRUPersistentCacheTest, ZeroSyncIntervalSyncsImmediately) {
489   EXPECT_CALL(*this->persistence, load_())
490       .Times(1)
491       .WillOnce(Return(dynamic::array()));
492   auto rawPersistence = this->persistence.get();
493   auto cache = createCacheWithExecutor<TypeParam>(
494       this->manualExecutor,
495       std::move(this->persistence),
496       std::chrono::milliseconds::zero(),
497       1,
498       true);
499   cache->init();
500   this->manualExecutor->run();
501   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
502       .WillRepeatedly(Invoke(
503           rawPersistence,
504           &MockPersistenceLayer::getLastPersistedVersionConcrete));
505 
506   cache->put("k0", "v0");
507   EXPECT_CALL(*rawPersistence, persist_(DynSize(1)))
508       .Times(1)
509       .WillOnce(Return(true));
510   this->manualExecutor->run();
511 
512   // The following put will trigger a sync because syncImmediatelyWithExecutor
513   // is set to true
514   EXPECT_CALL(*rawPersistence, getLastPersistedVersion())
515       .WillRepeatedly(Invoke(
516           rawPersistence,
517           &MockPersistenceLayer::getLastPersistedVersionConcrete));
518   EXPECT_CALL(*rawPersistence, persist_(DynSize(2)))
519       .Times(1)
520       .WillOnce(Return(true));
521   cache->put("k1", "v1");
522   this->manualExecutor->run();
523 
524   EXPECT_CALL(*rawPersistence, persist_(_)).Times(0);
525   cache.reset();
526   // Nothing more should happen after this
527   this->manualExecutor->drain();
528 }
529