Skip to content

Commit 1a94c2c

Browse files
authored
Merge pull request #10476 from Icinga/broken-config-stage-updates
Handle concurrent config package updates gracefully
2 parents c28076a + ce3275d commit 1a94c2c

File tree

4 files changed

+65
-25
lines changed

4 files changed

+65
-25
lines changed

lib/remote/configpackageshandler.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "remote/configpackageshandler.hpp"
44
#include "remote/configpackageutility.hpp"
5+
#include "remote/configobjectslock.hpp"
56
#include "remote/httputility.hpp"
67
#include "remote/filterutility.hpp"
78
#include "base/exception.hpp"
@@ -111,6 +112,12 @@ void ConfigPackagesHandler::HandlePost(
111112
return;
112113
}
113114

115+
ConfigObjectsSharedLock configObjectsSharedLock(std::try_to_lock);
116+
if (!configObjectsSharedLock) {
117+
HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
118+
return;
119+
}
120+
114121
try {
115122
std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
116123

@@ -157,6 +164,12 @@ void ConfigPackagesHandler::HandleDelete(
157164
return;
158165
}
159166

167+
ConfigObjectsSharedLock lock(std::try_to_lock);
168+
if (!lock) {
169+
HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
170+
return;
171+
}
172+
160173
try {
161174
ConfigPackageUtility::DeletePackage(packageName);
162175
} catch (const std::exception& ex) {

lib/remote/configpackageutility.cpp

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "remote/configpackageutility.hpp"
44
#include "remote/apilistener.hpp"
55
#include "base/application.hpp"
6+
#include "base/atomic-file.hpp"
67
#include "base/exception.hpp"
78
#include "base/utility.hpp"
89
#include <boost/algorithm/string.hpp>
@@ -119,14 +120,9 @@ String ConfigPackageUtility::CreateStage(const String& packageName, const Dictio
119120
void ConfigPackageUtility::WritePackageConfig(const String& packageName)
120121
{
121122
String stageName = GetActiveStage(packageName);
123+
AtomicFile::Write(GetPackageDir() + "/" + packageName + "/include.conf", 0644, "include \"*/include.conf\"\n");
122124

123-
String includePath = GetPackageDir() + "/" + packageName + "/include.conf";
124-
std::ofstream fpInclude(includePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
125-
fpInclude << "include \"*/include.conf\"\n";
126-
fpInclude.close();
127-
128-
String activePath = GetPackageDir() + "/" + packageName + "/active.conf";
129-
std::ofstream fpActive(activePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
125+
AtomicFile fpActive(GetPackageDir() + "/" + packageName + "/active.conf", 0644);
130126
fpActive << "if (!globals.contains(\"ActiveStages\")) {\n"
131127
<< " globals.ActiveStages = {}\n"
132128
<< "}\n"
@@ -145,19 +141,18 @@ void ConfigPackageUtility::WritePackageConfig(const String& packageName)
145141
<< "if (!ActiveStages.contains(\"" << packageName << "\")) {\n"
146142
<< " ActiveStages[\"" << packageName << "\"] = \"" << stageName << "\"\n"
147143
<< "}\n";
148-
fpActive.close();
144+
fpActive.Commit();
149145
}
150146

151147
void ConfigPackageUtility::WriteStageConfig(const String& packageName, const String& stageName)
152148
{
153-
String path = GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf";
154-
std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
149+
AtomicFile fp(GetPackageDir() + "/" + packageName + "/" + stageName + "/include.conf", 0644);
155150
fp << "include \"../active.conf\"\n"
156151
<< "if (ActiveStages[\"" << packageName << "\"] == \"" << stageName << "\") {\n"
157152
<< " include_recursive \"conf.d\"\n"
158153
<< " include_zones \"" << packageName << "\", \"zones.d\"\n"
159154
<< "}\n";
160-
fp.close();
155+
fp.Commit();
161156
}
162157

163158
void ConfigPackageUtility::ActivateStage(const String& packageName, const String& stageName)
@@ -284,12 +279,7 @@ String ConfigPackageUtility::GetActiveStageFromFile(const String& packageName)
284279
void ConfigPackageUtility::SetActiveStageToFile(const String& packageName, const String& stageName)
285280
{
286281
std::unique_lock<std::mutex> lock(GetStaticActiveStageMutex());
287-
288-
String activeStagePath = GetPackageDir() + "/" + packageName + "/active-stage";
289-
290-
std::ofstream fpActiveStage(activeStagePath.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc); //TODO: fstream exceptions
291-
fpActiveStage << stageName;
292-
fpActiveStage.close();
282+
AtomicFile::Write(GetPackageDir() + "/" + packageName + "/active-stage", 0644, stageName);
293283
}
294284

295285
String ConfigPackageUtility::GetActiveStage(const String& packageName)

lib/remote/configstageshandler.cpp

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "remote/configstageshandler.hpp"
44
#include "remote/configpackageutility.hpp"
5+
#include "remote/configobjectslock.hpp"
56
#include "remote/httputility.hpp"
67
#include "remote/filterutility.hpp"
78
#include "base/application.hpp"
@@ -12,7 +13,10 @@ using namespace icinga;
1213

1314
REGISTER_URLHANDLER("/v1/config/stages", ConfigStagesHandler);
1415

15-
std::atomic<bool> ConfigStagesHandler::m_RunningPackageUpdates (false);
16+
static bool l_RunningPackageUpdates(false);
17+
// A timestamp that indicates the last time an Icinga 2 reload failed.
18+
static double l_LastReloadFailedTime(0);
19+
static std::mutex l_RunningPackageUpdatesMutex; // Protects the above two variables.
1620

1721
bool ConfigStagesHandler::HandleRequest(
1822
const WaitGroup::Ptr&,
@@ -132,12 +136,42 @@ void ConfigStagesHandler::HandlePost(
132136
if (reload && !activate)
133137
BOOST_THROW_EXCEPTION(std::invalid_argument("Parameter 'reload' must be false when 'activate' is false."));
134138

135-
if (m_RunningPackageUpdates.exchange(true)) {
136-
return HttpUtility::SendJsonError(response, params, 423,
137-
"Conflicting request, there is already an ongoing package update in progress. Please try it again later.");
139+
ConfigObjectsSharedLock configObjectsSharedLock(std::try_to_lock);
140+
if (!configObjectsSharedLock) {
141+
HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
142+
return;
138143
}
139144

140-
auto resetPackageUpdates (Shared<Defer>::Make([]() { ConfigStagesHandler::m_RunningPackageUpdates.store(false); }));
145+
{
146+
std::lock_guard runningPackageUpdatesLock(l_RunningPackageUpdatesMutex);
147+
double currentReloadFailedTime = Application::GetLastReloadFailed();
148+
149+
/**
150+
* Once the m_RunningPackageUpdates flag is set, it typically remains set until the current worker process is
151+
* terminated, in which case the new worker will have its own m_RunningPackageUpdates flag set to false.
152+
* However, if the reload fails for any reason, the m_RunningPackageUpdates flag will remain set to true
153+
* in the current worker process, which will prevent any further package updates from being processed until
154+
* the next Icinga 2 restart.
155+
*
156+
* So, in order to prevent such a situation, we are additionally tracking the last time a reload failed
157+
* and allow to bypass the m_RunningPackageUpdates flag only if the last reload failed time was changed
158+
* since the previous request.
159+
*/
160+
if (l_RunningPackageUpdates && l_LastReloadFailedTime == currentReloadFailedTime) {
161+
return HttpUtility::SendJsonError(
162+
response, params, 423,
163+
"Conflicting request, there is already an ongoing package update in progress. Please try it again later."
164+
);
165+
}
166+
167+
l_RunningPackageUpdates = true;
168+
l_LastReloadFailedTime = currentReloadFailedTime;
169+
}
170+
171+
auto resetPackageUpdates (Shared<Defer>::Make([]() {
172+
std::lock_guard lock(l_RunningPackageUpdatesMutex);
173+
l_RunningPackageUpdates = false;
174+
}));
141175

142176
std::unique_lock<std::mutex> lock(ConfigPackageUtility::GetStaticPackageMutex());
143177

@@ -201,6 +235,12 @@ void ConfigStagesHandler::HandleDelete(
201235
if (!ConfigPackageUtility::ValidateStageName(stageName))
202236
return HttpUtility::SendJsonError(response, params, 400, "Invalid stage name '" + stageName + "'.");
203237

238+
ConfigObjectsSharedLock lock(std::try_to_lock);
239+
if (!lock) {
240+
HttpUtility::SendJsonError(response, params, 503, "Icinga is reloading");
241+
return;
242+
}
243+
204244
try {
205245
ConfigPackageUtility::DeleteStage(packageName, stageName);
206246
} catch (const std::exception& ex) {

lib/remote/configstageshandler.hpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#define CONFIGSTAGESHANDLER_H
55

66
#include "remote/httphandler.hpp"
7-
#include <atomic>
87

98
namespace icinga
109
{
@@ -48,8 +47,6 @@ class ConfigStagesHandler final : public HttpHandler
4847
boost::beast::http::response<boost::beast::http::string_body>& response,
4948
const Dictionary::Ptr& params
5049
);
51-
52-
static std::atomic<bool> m_RunningPackageUpdates;
5350
};
5451

5552
}

0 commit comments

Comments
 (0)