1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements. See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership. The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License. You may obtain a copy of the License at
9  *
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied. See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 #ifndef _GNU_SOURCE
20 #define _GNU_SOURCE // needed for getopt_long
21 #endif
22 
23 #include <thrift/thrift-config.h>
24 
25 #ifdef HAVE_SYS_TIME_H
26 #include <sys/time.h>
27 #endif
28 #include <getopt.h>
29 #include <boost/test/unit_test.hpp>
30 
31 #include <thrift/transport/TFileTransport.h>
32 
33 #ifdef __MINGW32__
34   #include <io.h>
35   #include <unistd.h>
36   #include <sys/types.h>
37   #include <fcntl.h>
38   #include <sys\stat.h>
39 #endif
40 
41 using namespace apache::thrift::transport;
42 
43 /**************************************************************************
44  * Global state
45  **************************************************************************/
46 
47 static const char* tmp_dir = "/tmp";
48 
49 class FsyncLog;
50 FsyncLog* fsync_log;
51 
52 /**************************************************************************
53  * Helper code
54  **************************************************************************/
55 
56 /**
57  * Class to record calls to fsync
58  */
59 class FsyncLog {
60 public:
61   struct FsyncCall {
62     struct timeval time;
63     int fd;
64   };
65   typedef std::list<FsyncCall> CallList;
66 
67   FsyncLog() = default;
68 
fsync(int fd)69   void fsync(int fd) {
70     (void)fd;
71     FsyncCall call;
72     THRIFT_GETTIMEOFDAY(&call.time, nullptr);
73     calls_.push_back(call);
74   }
75 
getCalls() const76   const CallList* getCalls() const { return &calls_; }
77 
78 private:
79   CallList calls_;
80 };
81 
82 /**
83  * Helper class to clean up temporary files
84  */
85 class TempFile {
86 public:
TempFile(const char * directory,const char * prefix)87   TempFile(const char* directory, const char* prefix) {
88   #ifdef __MINGW32__
89     ((void)directory);
90     size_t path_len = strlen(prefix) + 8;
91     path_ = new char[path_len];
92     snprintf(path_, path_len, "%sXXXXXX", prefix);
93     if (_mktemp_s(path_,path_len) == 0) {
94       fd_ = open(path_,O_CREAT | O_RDWR | O_BINARY,S_IREAD | S_IWRITE);
95       if (fd_ < 0) {
96         throw apache::thrift::TException("_mktemp_s() failed");
97       }
98     } else {
99       throw apache::thrift::TException("_mktemp_s() failed");
100     }
101   #else
102     size_t path_len = strlen(directory) + strlen(prefix) + 8;
103     path_ = new char[path_len];
104     snprintf(path_, path_len, "%s/%sXXXXXX", directory, prefix);
105 
106     fd_ = mkstemp(path_);
107     if (fd_ < 0) {
108       throw apache::thrift::TException("mkstemp() failed");
109     }
110   #endif
111   }
112 
~TempFile()113   ~TempFile() {
114     unlink();
115     close();
116   }
117 
getPath() const118   const char* getPath() const { return path_; }
119 
getFD() const120   int getFD() const { return fd_; }
121 
unlink()122   void unlink() {
123     if (path_) {
124       ::unlink(path_);
125       delete[] path_;
126       path_ = nullptr;
127     }
128   }
129 
close()130   void close() {
131     if (fd_ < 0) {
132       return;
133     }
134 
135     ::close(fd_);
136     fd_ = -1;
137   }
138 
139 private:
140   char* path_;
141   int fd_;
142 };
143 
144 // Use our own version of fsync() for testing.
145 // This returns immediately, so timing in test_destructor() isn't affected by
146 // waiting on the actual filesystem.
fsync(int fd)147 extern "C" int fsync(int fd) {
148   if (fsync_log) {
149     fsync_log->fsync(fd);
150   }
151   return 0;
152 }
153 
time_diff(const struct timeval * t1,const struct timeval * t2)154 int time_diff(const struct timeval* t1, const struct timeval* t2) {
155   return (t2->tv_usec - t1->tv_usec) + (t2->tv_sec - t1->tv_sec) * 1000000;
156 }
157 
158 /**************************************************************************
159  * Test cases
160  **************************************************************************/
161 
162 /**
163  * Make sure the TFileTransport destructor exits "quickly".
164  *
165  * Previous versions had a bug causing the writer thread not to exit
166  * right away.
167  *
168  * It's kind of lame that we just check to see how long the destructor takes in
169  * wall-clock time.  This could result in false failures on slower systems, or
170  * on heavily loaded machines.
171  */
BOOST_AUTO_TEST_CASE(test_destructor)172 BOOST_AUTO_TEST_CASE(test_destructor) {
173   TempFile f(tmp_dir, "thrift.TFileTransportTest.");
174 
175   unsigned int const NUM_ITERATIONS = 1000;
176 
177   unsigned int num_over = 0;
178   for (unsigned int n = 0; n < NUM_ITERATIONS; ++n) {
179     BOOST_CHECK_EQUAL(0, ftruncate(f.getFD(), 0));
180 
181     TFileTransport* transport = new TFileTransport(f.getPath());
182 
183     // write something so that the writer thread gets started
184     transport->write(reinterpret_cast<const uint8_t*>("foo"), 3);
185 
186     // Every other iteration, also call flush(), just in case that potentially
187     // has any effect on how the writer thread wakes up.
188     if (n & 0x1) {
189       transport->flush();
190     }
191 
192     /*
193      * Time the call to the destructor
194      */
195     struct timeval start;
196     struct timeval end;
197 
198     THRIFT_GETTIMEOFDAY(&start, nullptr);
199     delete transport;
200     THRIFT_GETTIMEOFDAY(&end, nullptr);
201 
202     int delta = time_diff(&start, &end);
203 
204     // If any attempt takes more than 500ms, treat that as a failure.
205     // Treat this as a fatal failure, so we'll return now instead of
206     // looping over a very slow operation.
207     BOOST_WARN( delta < 500000 );
208 
209     // Normally, it takes less than 100ms on my dev box.
210     // However, if the box is heavily loaded, some of the test runs
211     // take longer, since we're just waiting for our turn on the CPU.
212     if (delta > 100000) {
213       ++num_over;
214     }
215   }
216 
217   // Make sure fewer than 10% of the runs took longer than 1000us
218   BOOST_WARN(num_over < (NUM_ITERATIONS / 10));
219 }
220 
221 /**
222  * Make sure setFlushMaxUs() is honored.
223  */
test_flush_max_us_impl(uint32_t flush_us,uint32_t write_us,uint32_t test_us)224 void test_flush_max_us_impl(uint32_t flush_us, uint32_t write_us, uint32_t test_us) {
225   // TFileTransport only calls fsync() if data has been written,
226   // so make sure the write interval is smaller than the flush interval.
227   BOOST_WARN(write_us < flush_us);
228 
229   TempFile f(tmp_dir, "thrift.TFileTransportTest.");
230 
231   // Record calls to fsync()
232   FsyncLog log;
233   fsync_log = &log;
234 
235   TFileTransport* transport = new TFileTransport(f.getPath());
236   // Don't flush because of # of bytes written
237   transport->setFlushMaxBytes(0xffffffff);
238   uint8_t buf[] = "a";
239   uint32_t buflen = sizeof(buf);
240 
241   // Set the flush interval
242   transport->setFlushMaxUs(flush_us);
243 
244   // Make one call to write, to start the writer thread now.
245   // (If we just let the thread get created during our test loop,
246   // the thread creation sometimes takes long enough to make the first
247   // fsync interval fail the check.)
248   transport->write(buf, buflen);
249 
250   // Add one entry to the fsync log, just to mark the start time
251   log.fsync(-1);
252 
253   // Loop doing write(), sleep(), ...
254   uint32_t total_time = 0;
255   while (true) {
256     transport->write(buf, buflen);
257     if (total_time > test_us) {
258       break;
259     }
260     usleep(write_us);
261     total_time += write_us;
262   }
263 
264   delete transport;
265 
266   // Stop logging new fsync() calls
267   fsync_log = nullptr;
268 
269   // Examine the fsync() log
270   //
271   // TFileTransport uses pthread_cond_timedwait(), which only has millisecond
272   // resolution.  In my testing, it normally wakes up about 1 millisecond late.
273   // However, sometimes it takes a bit longer.  Allow 5ms leeway.
274   int max_allowed_delta = flush_us + 5000;
275 
276   const FsyncLog::CallList* calls = log.getCalls();
277   // We added 1 fsync call above.
278   // Make sure TFileTransport called fsync at least once
279   BOOST_WARN_GE(calls->size(), static_cast<FsyncLog::CallList::size_type>(1));
280 
281   const struct timeval* prev_time = nullptr;
282   for (const auto & call : *calls) {
283     if (prev_time) {
284       int delta = time_diff(prev_time, &call.time);
285       BOOST_WARN( delta < max_allowed_delta );
286     }
287     prev_time = &call.time;
288   }
289 }
290 
BOOST_AUTO_TEST_CASE(test_flush_max_us1)291 BOOST_AUTO_TEST_CASE(test_flush_max_us1) {
292   // fsync every 10ms, write every 5ms, for 500ms
293   test_flush_max_us_impl(10000, 5000, 500000);
294 }
295 
BOOST_AUTO_TEST_CASE(test_flush_max_us2)296 BOOST_AUTO_TEST_CASE(test_flush_max_us2) {
297   // fsync every 50ms, write every 20ms, for 500ms
298   test_flush_max_us_impl(50000, 20000, 500000);
299 }
300 
BOOST_AUTO_TEST_CASE(test_flush_max_us3)301 BOOST_AUTO_TEST_CASE(test_flush_max_us3) {
302   // fsync every 400ms, write every 300ms, for 1s
303   test_flush_max_us_impl(400000, 300000, 1000000);
304 }
305 
306 /**
307  * Make sure flush() is fast when there is nothing to do.
308  *
309  * TFileTransport used to have a bug where flush() would wait for the fsync
310  * timeout to expire.
311  */
BOOST_AUTO_TEST_CASE(test_noop_flush)312 BOOST_AUTO_TEST_CASE(test_noop_flush) {
313   TempFile f(tmp_dir, "thrift.TFileTransportTest.");
314   TFileTransport transport(f.getPath());
315 
316   // Write something to start the writer thread.
317   uint8_t buf[] = "a";
318   transport.write(buf, 1);
319 
320   struct timeval start;
321   THRIFT_GETTIMEOFDAY(&start, nullptr);
322 
323   for (unsigned int n = 0; n < 10; ++n) {
324     transport.flush();
325 
326     struct timeval now;
327     THRIFT_GETTIMEOFDAY(&now, nullptr);
328 
329     // Fail if at any point we've been running for longer than half a second.
330     // (With the buggy code, TFileTransport used to take 3 seconds per flush())
331     //
332     // Use a fatal fail so we break out early, rather than continuing to make
333     // many more slow flush() calls.
334     int delta = time_diff(&start, &now);
335     BOOST_WARN( delta < 2000000 );
336   }
337 }
338 
339 /**************************************************************************
340  * General Initialization
341  **************************************************************************/
342 
print_usage(FILE * f,const char * argv0)343 void print_usage(FILE* f, const char* argv0) {
344   fprintf(f, "Usage: %s [boost_options] [options]\n", argv0);
345   fprintf(f, "Options:\n");
346   fprintf(f, "  --tmp-dir=DIR, -t DIR\n");
347   fprintf(f, "  --help\n");
348 }
349 
parse_args(int argc,char * argv[])350 void parse_args(int argc, char* argv[]) {
351   struct option long_opts[]
352       = {{"help", false, nullptr, 'h'}, {"tmp-dir", true, nullptr, 't'}, {nullptr, 0, nullptr, 0}};
353 
354   while (true) {
355     optopt = 1;
356     int optchar = getopt_long(argc, argv, "ht:", long_opts, nullptr);
357     if (optchar == -1) {
358       break;
359     }
360 
361     switch (optchar) {
362     case 't':
363       tmp_dir = optarg;
364       break;
365     case 'h':
366       print_usage(stdout, argv[0]);
367       exit(0);
368     case '?':
369       exit(1);
370     default:
371       // Only happens if someone adds another option to the optarg string,
372       // but doesn't update the switch statement to handle it.
373       fprintf(stderr, "unknown option \"-%c\"\n", optchar);
374       exit(1);
375     }
376   }
377 }
378 
379 #ifdef BOOST_TEST_DYN_LINK
380 static int myArgc = 0;
381 static char **myArgv = nullptr;
382 
init_unit_test_suite()383 bool init_unit_test_suite() {
384   boost::unit_test::framework::master_test_suite().p_name.value = "TFileTransportTest";
385 
386   // Parse arguments
387   parse_args(myArgc,myArgv);
388   return true;
389 }
390 
main(int argc,char * argv[])391 int main( int argc, char* argv[] ) {
392   myArgc = argc;
393   myArgv = argv;
394   return ::boost::unit_test::unit_test_main(&init_unit_test_suite,argc,argv);
395 }
396 #else
init_unit_test_suite(int argc,char * argv[])397 boost::unit_test::test_suite* init_unit_test_suite(int argc, char* argv[]) {
398   boost::unit_test::framework::master_test_suite().p_name.value = "TFileTransportTest";
399 
400   // Parse arguments
401   parse_args(argc, argv);
402   return nullptr;
403 }
404 #endif
405