Skip to content

Commit b28fd27

Browse files
author
Max Fellows
committed
Safety improvements for ClickHouse: since ClickHouse does not support unique constraints or transactional DDL (even though pqxx doesn't complain), there's a race condition between recording that a job has completed and loading its results where the results loading could be interrupted after the job has been marked as completed by creating the original guard view.
This attempts to fix the race condition using a file-based mutex to guard against duplicate jobs loading at the same time, and using a single transaction for the completed job tracking and the results load to emulate the original Postgres-only implementation of distributed runs.
1 parent 42ee951 commit b28fd27

2 files changed

Lines changed: 33 additions & 65 deletions

File tree

Source/moja.modules.cbm/include/moja/modules/cbm/cbmaggregatorhybridlibpqxxwriter.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ namespace cbm {
6969
const std::string& table,
7070
std::shared_ptr<TAccumulator> dataDimension);
7171

72-
void doIsolated(pqxx::connection_base& conn, std::string sql, bool optional = false);
73-
void doIsolated(pqxx::connection_base& conn, std::vector<std::string> sql, bool optional = false);
72+
bool checkCompleted(pqxx::connection_base& conn);
7473
};
7574

7675
}}} // namespace moja::modules::cbm

Source/moja.modules.cbm/src/cbmaggregatorhybridlibpqxxwriter.cpp

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
/**
2-
* @file
3-
* The CBMAggregatorHybridLibPQXXWriter module writes the stand-level information gathered
4-
* by CBMAggregatorLandUnitData into a PostgreSQL database. It is designed mainly for
5-
* distributed runs where the simulation is divided up and each portion of work is loaded
6-
* into a separate set of tables before being merged together with a post-processing script,
7-
* although this module can also be used for a standard simulation
8-
********/
9-
101
#include "moja/modules/cbm/cbmaggregatorhybridlibpqxxwriter.h"
112

123
#include <moja/flint/recordaccumulatorwithmutex.h>
@@ -22,6 +13,8 @@
2213
#include <boost/algorithm/string.hpp>
2314
#include <boost/algorithm/string/replace.hpp>
2415
#include <boost/format.hpp>
16+
#include <boost/interprocess/sync/scoped_lock.hpp>
17+
#include <boost/interprocess/sync/file_lock.hpp>
2518

2619
using namespace pqxx;
2720
using Poco::format;
@@ -63,72 +56,48 @@ namespace cbm {
6356
MOJA_LOG_INFO << (boost::format("Loading results into %1% on server: %2%")
6457
% _schema % _chConnectionString).str();
6558

66-
connection chConn(_chConnectionString);
67-
68-
std::string guardTable = (boost::format("completed_%1%") % _jobId).str();
69-
bool resultsPreviouslyLoaded = perform([&chConn, &guardTable, this] {
70-
work tx(chConn);
71-
row result = tx.exec1((boost::format(
72-
"EXISTS TABLE %1%.%2%;"
73-
) % _schema % guardTable).str());
74-
75-
tx.commit();
76-
return result[0].as<int>() == 1;
77-
});
59+
connection conn(_chConnectionString);
7860

79-
if (resultsPreviouslyLoaded) {
61+
if (checkCompleted(conn)) {
8062
MOJA_LOG_INFO << "Results previously loaded for jobId " << _jobId << " - skipping.";
8163
return;
8264
}
8365

84-
perform([&chConn, &guardTable, this] {
85-
work chTx(chConn);
66+
{
67+
std::string lockName = (boost::format("%1%.lock") % _jobId).str();
68+
boost::interprocess::scoped_lock lock{
69+
boost::interprocess::file_lock{lockName.c_str()}
70+
};
8671

87-
// ClickHouse doesn't support unique constraints, so the guard against
88-
// duplicate loads for the same job has to be a table. If this is a
89-
// duplicate, the transaction will fail and roll back the data load.
90-
chTx.exec((boost::format("CREATE VIEW %1%.%2% AS SELECT 1;") % _schema % guardTable).str());
91-
92-
load(chTx, (boost::format("%1%.raw_fluxes") % _schema).str(), _fluxDimension);
93-
load(chTx, (boost::format("%1%.raw_pools") % _schema).str(), _poolDimension);
94-
load(chTx, (boost::format("%1%.raw_errors") % _schema).str(), _errorDimension);
95-
load(chTx, (boost::format("%1%.raw_ages") % _schema).str(), _ageDimension);
96-
load(chTx, (boost::format("%1%.raw_disturbances") % _schema).str(), _disturbanceDimension);
97-
98-
chTx.commit();
99-
});
100-
101-
MOJA_LOG_INFO << "PostgreSQL insert complete." << std::endl;
102-
}
72+
perform([&conn, this] {
73+
if (checkCompleted(conn)) {
74+
MOJA_LOG_INFO << "Results previously loaded for jobId " << _jobId << " - skipping.";
75+
return;
76+
}
10377

104-
void CBMAggregatorHybridLibPQXXWriter::doIsolated(pqxx::connection_base& conn, std::string sql, bool optional) {
105-
perform([&conn, sql, optional] {
106-
try {
10778
work tx(conn);
108-
tx.exec(sql);
79+
tx.exec((boost::format("INSERT INTO %1%.completed_jobs VALUES (%2%);") % _schema % _jobId).str());
80+
load(tx, (boost::format("%1%.raw_fluxes") % _schema).str(), _fluxDimension);
81+
load(tx, (boost::format("%1%.raw_pools") % _schema).str(), _poolDimension);
82+
load(tx, (boost::format("%1%.raw_errors") % _schema).str(), _errorDimension);
83+
load(tx, (boost::format("%1%.raw_ages") % _schema).str(), _ageDimension);
84+
load(tx, (boost::format("%1%.raw_disturbances") % _schema).str(), _disturbanceDimension);
10985
tx.commit();
110-
} catch (...) {
111-
if (!optional) {
112-
throw;
113-
}
114-
}
115-
});
86+
});
87+
}
88+
89+
MOJA_LOG_INFO << "Insert complete." << std::endl;
11690
}
11791

118-
void CBMAggregatorHybridLibPQXXWriter::doIsolated(pqxx::connection_base& conn, std::vector<std::string> sql, bool optional) {
119-
perform([&conn, sql, optional] {
120-
try {
121-
work tx(conn);
122-
for (auto stmt : sql) {
123-
tx.exec(stmt);
124-
}
92+
bool CBMAggregatorHybridLibPQXXWriter::checkCompleted(pqxx::connection_base& conn) {
93+
return perform([&conn, this] {
94+
work tx(conn);
95+
row result = tx.exec1((boost::format(
96+
"SELECT EXISTS (SELECT * FROM %1%.completed_jobs WHERE id = %2%);"
97+
) % _schema % _jobId).str());
12598

126-
tx.commit();
127-
} catch (...) {
128-
if (!optional) {
129-
throw;
130-
}
131-
}
99+
tx.commit();
100+
return result[0].as<int>() == 1;
132101
});
133102
}
134103

0 commit comments

Comments
 (0)