1 /*
2   BAREOS® - Backup Archiving REcovery Open Sourced
3 
4   Copyright (C) 2019-2020 Bareos GmbH & Co. KG
5 
6   This program is Free Software; you can redistribute it and/or
7   modify it under the terms of version three of the GNU Affero General Public
8   License as published by the Free Software Foundation, which is
9   listed in the file LICENSE.
10 
11   This program is distributed in the hope that it will be useful, but
12   WITHOUT ANY WARRANTY; without even the implied warranty of
13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14   Affero General Public License for more details.
15 
16   You should have received a copy of the GNU Affero General Public License
17   along with this program; if not, write to the Free Software
18   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19   02110-1301, USA.
20 */
21 
22 #if defined(HAVE_MINGW)
23 #  include "include/bareos.h"
24 #  include "gtest/gtest.h"
25 #else
26 #  include "gtest/gtest.h"
27 #  include "include/bareos.h"
28 #endif
29 
30 
31 #include "dird/dird_globals.h"
32 #include "dird/jcr_private.h"
33 #include "dird/scheduler.h"
34 #include "dird/dird_conf.h"
35 #define DIRECTOR_DAEMON
36 #include "include/jcr.h"
37 #include "include/make_unique.h"
38 #include "lib/parse_conf.h"
39 #include "tests/scheduler_time_source.h"
40 #include "dird/scheduler_system_time_source.h"
41 
42 #include <atomic>
43 #include <chrono>
44 #include <iostream>
45 #include <iterator>
46 #include <numeric>
47 #include <sstream>
48 #include <string>
49 #include <thread>
50 
51 using namespace directordaemon;
52 
53 namespace directordaemon {
DoReloadConfig()54 bool DoReloadConfig() { return false; }
55 }  // namespace directordaemon
56 
57 class SimulatedTimeAdapter : public SchedulerTimeAdapter {
58  public:
SimulatedTimeAdapter()59   SimulatedTimeAdapter()
60       : SchedulerTimeAdapter(std::make_unique<SimulatedTimeSource>())
61   {
62     default_wait_interval_ = 60;
63   }
64 };
65 
66 static std::unique_ptr<Scheduler> scheduler;
67 static SimulatedTimeAdapter* time_adapter;
68 
69 class SchedulerTest : public ::testing::Test {
SetUp()70   void SetUp() override
71   {
72     OSDependentInit();
73     std::unique_ptr<SimulatedTimeAdapter> ta
74         = std::make_unique<SimulatedTimeAdapter>();
75     time_adapter = ta.get();
76 
77     scheduler = std::make_unique<Scheduler>(std::move(ta),
78                                             SimulatedTimeSource::ExecuteJob);
79   }
TearDown()80   void TearDown() override { scheduler.reset(); }
81 };
82 
StopScheduler(std::chrono::milliseconds timeout)83 static void StopScheduler(std::chrono::milliseconds timeout)
84 {
85   std::this_thread::sleep_for(timeout);
86   scheduler->Terminate();
87 }
88 
TEST_F(SchedulerTest,terminate)89 TEST_F(SchedulerTest, terminate)
90 {
91   InitMsg(NULL, NULL); /* initialize message handler */
92 
93   std::string path_to_config_file
94       = std::string(RELATIVE_PROJECT_SOURCE_DIR "/configs/scheduler-hourly");
95 
96   my_config = InitDirConfig(path_to_config_file.c_str(), M_ERROR_TERM);
97   ASSERT_TRUE(my_config);
98 
99   my_config->ParseConfig();
100 
101   std::thread scheduler_canceler(StopScheduler, std::chrono::milliseconds(200));
102 
103   scheduler->Run();
104 
105   scheduler_canceler.join();
106   delete my_config;
107 }
108 
TEST_F(SchedulerTest,system_time_source)109 TEST_F(SchedulerTest, system_time_source)
110 {
111   SystemTimeSource s;
112   time_t start = s.SystemTime();
113   s.SleepFor(std::chrono::seconds(1));
114   time_t end = s.SystemTime();
115   EXPECT_GT(end - start, 0);
116   EXPECT_LT(end - start, 3);
117 }
118 
TEST_F(SchedulerTest,system_time_source_canceled)119 TEST_F(SchedulerTest, system_time_source_canceled)
120 {
121   SystemTimeSource s;
122 
123   time_t start = s.SystemTime();
124 
125   std::thread terminate_wait_for_thread([&s]() {
126     std::this_thread::sleep_for(std::chrono::milliseconds(50));
127     s.Terminate();
128   });
129 
130   s.SleepFor(std::chrono::seconds(1));
131   time_t end = s.SystemTime();
132 
133   terminate_wait_for_thread.join();
134 
135   bool end_equals_start = (end == start);
136   bool end_wrapped_to_next_second_while_waiting = (end == start + 1);
137 
138   EXPECT_TRUE(end_equals_start || end_wrapped_to_next_second_while_waiting);
139 }
140 
141 static std::vector<time_t> list_of_job_execution_time_stamps;
142 static std::vector<time_t> list_of_time_gaps_between_adjacent_jobs;
143 static int maximum_number_of_jobs_run{0};
144 static int counter_of_number_of_jobs_run{0};
145 
ExecuteJob(JobControlRecord * jcr)146 void SimulatedTimeSource::ExecuteJob(JobControlRecord* jcr)
147 {
148   // called by the scheduler to simulate a job run
149   counter_of_number_of_jobs_run++;
150 
151   list_of_job_execution_time_stamps.emplace_back(
152       time_adapter->time_source_->SystemTime());
153 
154   if (debug) { std::cout << jcr->impl->res.job->resource_name_ << std::endl; }
155 
156   if (counter_of_number_of_jobs_run == maximum_number_of_jobs_run) {
157     scheduler->Terminate();
158   }
159 
160   // auto tm = *std::gmtime(&t);
161   // std::cout << "Executing job: " << jcr->Job << std::endl;
162   // std::cout << put_time(&tm, "%d-%m-%Y %H:%M:%S") << std::endl;
163 }
164 
CalculateAverage()165 static int CalculateAverage()
166 {
167   std::adjacent_difference(
168       list_of_job_execution_time_stamps.begin(),
169       list_of_job_execution_time_stamps.end(),
170       std::back_inserter(list_of_time_gaps_between_adjacent_jobs));
171 
172   list_of_time_gaps_between_adjacent_jobs.erase(
173       list_of_time_gaps_between_adjacent_jobs.begin());
174 
175   int sum{std::accumulate(list_of_time_gaps_between_adjacent_jobs.begin(),
176                           list_of_time_gaps_between_adjacent_jobs.end(), 0)};
177 
178   return sum / list_of_time_gaps_between_adjacent_jobs.size();
179 }
180 
TEST_F(SchedulerTest,hourly)181 TEST_F(SchedulerTest, hourly)
182 {
183   InitMsg(NULL, NULL);
184 
185   if (debug) { std::cout << "Start test" << std::endl; }
186 
187   std::string path_to_config_file{
188       std::string(RELATIVE_PROJECT_SOURCE_DIR "/configs/scheduler-hourly")};
189 
190   my_config = InitDirConfig(path_to_config_file.c_str(), M_ERROR_TERM);
191   ASSERT_TRUE(my_config);
192 
193   if (debug) { std::cout << "Parse config" << std::endl; }
194 
195   my_config->ParseConfig();
196   ASSERT_TRUE(PopulateDefs());
197 
198   list_of_job_execution_time_stamps.clear();
199   list_of_time_gaps_between_adjacent_jobs.clear();
200   counter_of_number_of_jobs_run = 0;
201   maximum_number_of_jobs_run = 25;
202 
203   if (debug) { std::cout << "Run scheduler" << std::endl; }
204   scheduler->Run();
205 
206   if (debug) { std::cout << "End" << std::endl; }
207   delete my_config;
208 
209   int average{CalculateAverage()};
210 
211   bool average_time_between_adjacent_jobs_is_too_low = average < 3600 - 36;
212   bool average_time_between_adjacent_jobs_is_too_high = average > 3600 + 36;
213 
214   if (debug || average_time_between_adjacent_jobs_is_too_low
215       || average_time_between_adjacent_jobs_is_too_high) {
216     int hour = 0;
217     for (const auto& t : list_of_job_execution_time_stamps) {
218       auto tm = *std::gmtime(&t);
219       std::cout << "Hour " << hour << ": "
220                 << put_time(&tm, "%d-%m-%Y %H:%M:%S");
221       if (hour) {
222         std::cout << " - " << list_of_time_gaps_between_adjacent_jobs[hour]
223                   << " sec";
224       }
225       std::cout << std::endl;
226       hour++;
227     }
228   }
229 
230   if (debug) {
231     std::cout << "Average simulated time between two jobs: " << average
232               << " seconds" << std::endl;
233   }
234 
235   EXPECT_FALSE(average_time_between_adjacent_jobs_is_too_low);
236   EXPECT_FALSE(average_time_between_adjacent_jobs_is_too_high);
237 }
238 
239 
TestWithConfig(std::string path_to_config_file,std::vector<uint8_t> wdays)240 static void TestWithConfig(std::string path_to_config_file,
241                            std::vector<uint8_t> wdays)
242 {
243   InitMsg(NULL, NULL);
244 
245   if (debug) { std::cout << "Start test" << std::endl; }
246 
247   my_config = InitDirConfig(path_to_config_file.c_str(), M_ERROR_TERM);
248   ASSERT_TRUE(my_config);
249 
250   if (debug) { std::cout << "Parse config" << std::endl; }
251 
252   my_config->ParseConfig();
253   ASSERT_TRUE(PopulateDefs());
254 
255   list_of_job_execution_time_stamps.clear();
256   maximum_number_of_jobs_run = 3;
257   counter_of_number_of_jobs_run = 0;
258 
259   if (debug) { std::cout << "Run scheduler" << std::endl; }
260   scheduler->Run();
261 
262   int wday_index = 0;
263   for (const auto& t : list_of_job_execution_time_stamps) {
264     auto tm = *std::localtime(&t);
265     bool is_two_o_clock = tm.tm_hour == 2;
266 
267     ASSERT_LT(wday_index, wdays.size())
268         << "List of weekdays should match the number of execution time stamps.";
269     bool is_right_day = wdays.at(wday_index) == tm.tm_wday;
270 
271     EXPECT_TRUE(is_two_o_clock);
272     EXPECT_TRUE(is_right_day);
273     if (debug || !is_two_o_clock || !is_right_day) {
274       std::cout << put_time(&tm, "%A, %d-%m-%Y %H:%M:%S ") << std::endl;
275     }
276     wday_index++;
277   }
278 
279   if (debug) { std::cout << "End" << std::endl; }
280   delete my_config;
281 }
282 
283 enum
284 {
285   kMonday = 1,
286   kTuesday = 2,
287   kWednesday = 3,
288   kThursday = 4,
289   kFriday = 5,
290   kSaturday = 6,
291   kSunday = 7
292 };
293 
TEST_F(SchedulerTest,on_time)294 TEST_F(SchedulerTest, on_time)
295 {
296   std::vector<uint8_t> wdays{kMonday, kMonday, kMonday};
297   TestWithConfig(RELATIVE_PROJECT_SOURCE_DIR "/configs/scheduler-on-time",
298                  wdays);
299 }
300 
TEST_F(SchedulerTest,on_time_noday)301 TEST_F(SchedulerTest, on_time_noday)
302 {
303   std::vector<uint8_t> wdays{kThursday, kFriday, kSaturday};
304   TestWithConfig(RELATIVE_PROJECT_SOURCE_DIR "/configs/scheduler-on-time-noday",
305                  wdays);
306 }
307 
TEST_F(SchedulerTest,on_time_noday_noclient)308 TEST_F(SchedulerTest, on_time_noday_noclient)
309 {
310   std::vector<uint8_t> wdays{kThursday, kFriday, kSaturday};
311   TestWithConfig(RELATIVE_PROJECT_SOURCE_DIR
312                  "/configs/scheduler-on-time-noday-noclient",
313                  wdays);
314 }
315 
TEST_F(SchedulerTest,add_job_with_no_run_resource_to_queue)316 TEST_F(SchedulerTest, add_job_with_no_run_resource_to_queue)
317 {
318   InitMsg(NULL, NULL);
319 
320   if (debug) { std::cout << "Start test" << std::endl; }
321 
322   std::string path_to_config_file{std::string(
323       RELATIVE_PROJECT_SOURCE_DIR "/configs/bareos-configparser-tests")};
324 
325   my_config = InitDirConfig(path_to_config_file.c_str(), M_ERROR_TERM);
326   ASSERT_TRUE(my_config);
327 
328   if (debug) { std::cout << "Parse config" << std::endl; }
329 
330   my_config->ParseConfig();
331   ASSERT_TRUE(PopulateDefs());
332 
333   counter_of_number_of_jobs_run = 0;
334   maximum_number_of_jobs_run = 1;
335 
336   std::thread scheduler_thread{[]() { scheduler->Run(); }};
337 
338   JobResource* job{dynamic_cast<JobResource*>(
339       my_config->GetResWithName(R_JOB, "backup-bareos-fd"))};
340   ASSERT_TRUE(job) << "Job Resource \"backup-bareos-fd\" not found";
341 
342   scheduler->AddJobWithNoRunResourceToQueue(job, JobTrigger::kUndefined);
343 
344   scheduler_thread.join();
345   ASSERT_EQ(counter_of_number_of_jobs_run, 1);
346 
347   delete my_config;
348 }
349