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 <fb303/ExportedStatMapImpl.h>
18 
19 #include <fb303/DynamicCounters.h>
20 #include <gtest/gtest.h>
21 #include <atomic>
22 #include <thread>
23 
24 #include <time.h>
25 using namespace std;
26 using namespace facebook;
27 using namespace facebook::fb303;
28 
testExportedStatMapImpl(bool useStatPtr)29 void testExportedStatMapImpl(bool useStatPtr) {
30   DynamicCounters dc;
31   ExportedStatMapImpl statMap(&dc);
32 
33   int64_t now = ::time(nullptr);
34   if (useStatPtr) {
35     ExportedStatMapImpl::StatPtr item = statMap.getStatPtr("test_value");
36     statMap.addValue(item, now, 10);
37   } else {
38     statMap.addValue("test_value", now, 10);
39   }
40 
41   int64_t tmp;
42   EXPECT_FALSE(dc.getCounter("FAKE_test_value.avg.60", &tmp));
43 
44   map<string, int64_t> res;
45   dc.getCounters(&res);
46   FOR_EACH (it, res) {
47     LOG(INFO) << "res[\"" << it->first << "\"] = " << it->second;
48   }
49   EXPECT_EQ(res.size(), 4);
50   EXPECT_EQ(res["test_value.avg.60"], 10);
51 
52   EXPECT_TRUE(dc.getCounter("test_value.avg.60", &tmp));
53   EXPECT_EQ(tmp, 10);
54   // now remove the stat
55   statMap.unExportStatAll("test_value");
56   res.clear();
57   dc.getCounters(&res);
58   EXPECT_TRUE(res.empty());
59 }
60 
TEST(ExportedStatMapImpl,ExportedBasics)61 TEST(ExportedStatMapImpl, ExportedBasics) {
62   testExportedStatMapImpl(false);
63 }
64 
TEST(ExportedStatMapImpl,ExportedLockAndUpdate)65 TEST(ExportedStatMapImpl, ExportedLockAndUpdate) {
66   testExportedStatMapImpl(true);
67 }
68 
69 // Destroy a timeseries stat without first unexporting its exported counters
TEST(ExportedStatMapImpl,ForgetWithoutUnexport)70 TEST(ExportedStatMapImpl, ForgetWithoutUnexport) {
71   DynamicCounters dc;
72   ExportedStatMapImpl statMap(&dc);
73 
74   const string statName = "mystat";
75   statMap.addValue(statName, 0, 10);
76   statMap.addValue(statName, 0, 20);
77   statMap.forgetStatsFor(statName);
78 
79   int64_t result;
80   string queryName = statName + ".avg";
81   dc.getCounter(queryName, &result);
82   EXPECT_EQ(result, 15);
83 }
84 
85 // Have one thread continuously call forgetStatsFor(),
86 // while another thread is continuously updating (and re-exporting) the stat,
87 // and a third thread is querying the average value.
TEST(ExportedStatMapImpl,ConcurrentForget)88 TEST(ExportedStatMapImpl, ConcurrentForget) {
89   DynamicCounters dc;
90   ExportedStatMapImpl statMap(&dc);
91 
92   const string statName = "mystat";
93 
94   uint64_t updateIterations = 0;
95   uint64_t forgetIterations = 0;
96   uint64_t queryIterations = 0;
97 
98   // Run for 2 seconds
99   auto end = std::chrono::steady_clock::now() + std::chrono::seconds(2);
100   std::atomic<bool> done(false);
101   std::thread updateThread([&] {
102     while (std::chrono::steady_clock::now() < end) {
103       for (int n = 0; n < 100; ++n) {
104         statMap.addValue(statName, 0, 1);
105         ++updateIterations;
106       }
107     }
108     done = true;
109   });
110 
111   std::thread forgetThread([&] {
112     while (!done) {
113       statMap.forgetStatsFor(statName);
114       ++forgetIterations;
115     }
116   });
117 
118   std::thread queryThread([&] {
119     string queryName = statName + ".avg";
120     while (!done) {
121       int64_t result;
122       dc.getCounter(queryName, &result);
123       ++queryIterations;
124     }
125   });
126 
127   updateThread.join();
128   forgetThread.join();
129   queryThread.join();
130 
131   // Just for sanity, make sure each thread ran through some
132   // minimal number of iterations.
133   EXPECT_GT(updateIterations, 1000);
134   EXPECT_GT(forgetIterations, 1000);
135   EXPECT_GT(queryIterations, 1000);
136 }
137 
exportStatThread(ExportedStatMapImpl * statsMap,const std::string & counterName,uint32_t numIters,uint64_t incrAmount)138 void exportStatThread(
139     ExportedStatMapImpl* statsMap,
140     const std::string& counterName,
141     uint32_t numIters,
142     uint64_t incrAmount) {
143   for (uint32_t n = 0; n < numIters; ++n) {
144     statsMap->exportStat(counterName, fb303::SUM);
145     statsMap->addValue(counterName, ::time(nullptr), incrAmount);
146     sched_yield();
147   }
148 }
149 
150 // Test calling exportStat() simultaneously from multiple threads,
151 // while the stat is also being updated.
TEST(ExportedStatMapImpl,MultithreadedExport)152 TEST(ExportedStatMapImpl, MultithreadedExport) {
153   DynamicCounters counters;
154   ExportedStatMapImpl statsMap(&counters);
155 
156   std::string counterName = "foo";
157   uint32_t numIters = 1000;
158   uint32_t numThreads = 4;
159   uint64_t incrAmount = 8;
160 
161   std::vector<std::thread> threads;
162   for (uint32_t n = 0; n < numThreads; ++n) {
163     threads.emplace_back(
164         exportStatThread, &statsMap, counterName, numIters, incrAmount);
165   }
166   for (auto& thread : threads) {
167     thread.join();
168   }
169 
170   auto lockedObj = statsMap.getLockedStatPtr(counterName);
171   EXPECT_EQ(numIters * numThreads * incrAmount, lockedObj->sum(0));
172 }
173 
TEST(LockableStat,Swap)174 TEST(LockableStat, Swap) {
175   using LockableStat = ExportedStatMapImpl::LockableStat;
176   DynamicCounters dc;
177   ExportedStatMapImpl statMap(&dc);
178 
179   LockableStat statA = statMap.getLockableStat("testA");
180   {
181     auto guard = statA.lock();
182     statA.addValueLocked(guard, ::time(nullptr), 10);
183     statA.flushLocked(guard);
184   }
185   LockableStat statB = statMap.getLockableStat("testB");
186 
187   {
188     auto guard = statA.lock();
189     EXPECT_EQ(guard->sum(0), 10);
190     guard = statB.lock();
191     EXPECT_EQ(guard->sum(0), 0);
192   }
193 
194   statA.swap(statB);
195 
196   // the stats are reversed now
197   // so acquire the locks in the B/A order
198   // to avoid a TSAN lock-order-inversion message
199   auto guard = statB.lock();
200   EXPECT_EQ(guard->sum(0), 10);
201   guard = statA.lock();
202   EXPECT_EQ(guard->sum(0), 0);
203 }
204 
TEST(LockableStat,AddValue)205 TEST(LockableStat, AddValue) {
206   using LockableStat = ExportedStatMapImpl::LockableStat;
207   DynamicCounters dc;
208   ExportedStatMapImpl statMap(&dc);
209 
210   time_t now = ::time(nullptr);
211   const string name = "test_value";
212   LockableStat stat = statMap.getLockableStat(name);
213   stat.addValue(now, 10);
214 
215   int64_t result;
216   string queryname = name + ".avg";
217   dc.getCounter(queryname, &result);
218   EXPECT_EQ(result, 10);
219   {
220     auto guard = stat.lock();
221     stat.addValueLocked(guard, now, 20);
222   }
223   dc.getCounter(queryname, &result);
224   EXPECT_EQ(result, 15);
225 }
226