Skip to content

Commit ed3034f

Browse files
authored
Migration of persistent data from previous versions (#355)
* add migration and cleaning of configs; add unit tests * keep client-state.jsn; opstore handled in migration * update changelog * fix UBSan error * reduce Unit test log verbosity (faster execution)
1 parent a9213ec commit ed3034f

File tree

10 files changed

+223
-27
lines changed

10 files changed

+223
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Input validation for unsigned int Configs ([#344](https://github.com/matth-x/MicroOcpp/pull/344))
2222
- Support for TransactionMessageAttempts/-RetryInterval ([#345](https://github.com/matth-x/MicroOcpp/pull/345))
2323
- Heap profiler and custom allocator support ([#350](https://github.com/matth-x/MicroOcpp/pull/350))
24+
- Migration of persistent storage ([#355](https://github.com/matth-x/MicroOcpp/pull/355))
2425

2526
### Removed
2627

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ target_compile_definitions(mo_unit_tests PUBLIC
180180
MO_PLATFORM=MO_PLATFORM_UNIX
181181
MO_NUMCONNECTORS=3
182182
MO_CUSTOM_TIMER
183-
MO_DBG_LEVEL=MO_DL_DEBUG
183+
MO_DBG_LEVEL=MO_DL_INFO
184184
MO_TRAFFIC_OUT
185185
MO_FILENAME_PREFIX="./mo_store/"
186186
MO_LocalAuthListMaxLength=8

src/MicroOcpp.cpp

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,12 @@ void mocpp_initialize(Connection& connection, const char *bootNotificationCreden
259259
BootService::loadBootStats(filesystem, bootstats);
260260

261261
if (autoRecover && bootstats.getBootFailureCount() > 3) {
262-
MO_DBG_ERR("multiple initialization failures detected");
263-
if (filesystem) {
264-
bool success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool {
265-
return !strncmp(fname, "sd", strlen("sd")) ||
266-
!strncmp(fname, "tx", strlen("tx")) ||
267-
!strncmp(fname, "op", strlen("op")) ||
268-
!strncmp(fname, "sc-", strlen("sc-")) ||
269-
!strncmp(fname, "reservation", strlen("reservation"));
270-
});
271-
MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed");
272-
273-
bootstats = BootStats();
274-
}
262+
BootService::recover(filesystem, bootstats);
263+
bootstats = BootStats();
275264
}
276265

266+
BootService::migrate(filesystem, bootstats);
267+
277268
bootstats.bootNr++; //assign new boot number to this run
278269
BootService::storeBootStats(filesystem, bootstats);
279270

@@ -373,13 +364,8 @@ void mocpp_initialize(Connection& connection, const char *bootNotificationCreden
373364
}
374365
credsJson.reset();
375366

376-
auto mocppVersion = declareConfiguration<const char*>("MicroOcppVersion", MO_VERSION, MO_KEYVALUE_FN, false, false, false);
377-
378367
configuration_load();
379368

380-
if (mocppVersion) {
381-
mocppVersion->setString(MO_VERSION);
382-
}
383369
MO_DBG_INFO("initialized MicroOcpp v" MO_VERSION);
384370
}
385371

src/MicroOcpp/Core/Configuration.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,11 @@ bool configuration_save() {
239239
return success;
240240
}
241241

242+
bool configuration_clean_unused() {
243+
for (auto& container : configurationContainers) {
244+
container->removeUnused();
245+
}
246+
return configuration_save();
247+
}
248+
242249
} //end namespace MicroOcpp

src/MicroOcpp/Core/Configuration.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,7 @@ bool configuration_load(const char *filename = nullptr);
3636

3737
bool configuration_save();
3838

39+
bool configuration_clean_unused(); //remove configs which haven't been accessed
40+
3941
} //end namespace MicroOcpp
4042
#endif

src/MicroOcpp/Core/ConfigurationContainer.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class ConfigurationContainer {
3535
virtual std::shared_ptr<Configuration> getConfiguration(const char *key) = 0;
3636

3737
virtual void loadStaticKey(Configuration& config, const char *key) { } //possible optimization: can replace internal key with passed static key
38+
39+
virtual void removeUnused() { } //remove configs which haven't been accessed (optional and only if known)
3840
};
3941

4042
class ConfigurationContainerVolatile : public ConfigurationContainer, public MemoryManaged {

src/MicroOcpp/Core/ConfigurationContainerFlash.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,25 @@ class ConfigurationContainerFlash : public ConfigurationContainer, public Memory
334334
config.setKey(key);
335335
clearKeyPool(key);
336336
}
337+
338+
void removeUnused() override {
339+
//if a config's key is still in the keyPool, we know it's unused because it has never been declared in FW (originates from an older FW version)
340+
341+
auto key = keyPool.begin();
342+
while (key != keyPool.end()) {
343+
344+
for (auto config = configurations.begin(); config != configurations.end(); ++config) {
345+
if ((*config)->getKey() == *key) {
346+
MO_DBG_DEBUG("remove unused config %s", (*config)->getKey());
347+
configurations.erase(config);
348+
break;
349+
}
350+
}
351+
352+
MO_FREE(*key);
353+
key = keyPool.erase(key);
354+
}
355+
}
337356
};
338357

339358
std::unique_ptr<ConfigurationContainer> makeConfigurationContainerFlash(std::shared_ptr<FilesystemAdapter> filesystem, const char *filename, bool accessible) {

src/MicroOcpp/Model/Boot/BootService.cpp

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
#include <MicroOcpp/Platform.h>
1515
#include <MicroOcpp/Debug.h>
1616

17-
#ifndef MO_BOOTSTATS_LONGTIME_MS
18-
#define MO_BOOTSTATS_LONGTIME_MS 180 * 1000
19-
#endif
20-
2117
using namespace MicroOcpp;
2218

2319
unsigned int PreBootQueue::getFrontRequestOpNr() {
@@ -71,6 +67,9 @@ void BootService::loop() {
7167
if (!executedLongTime && mocpp_tick_ms() - firstExecutionTimestamp >= MO_BOOTSTATS_LONGTIME_MS) {
7268
executedLongTime = true;
7369
MO_DBG_DEBUG("boot success timer reached");
70+
71+
configuration_clean_unused();
72+
7473
BootStats bootstats;
7574
loadBootStats(filesystem, bootstats);
7675
bootstats.lastBootSuccess = bootstats.bootNr;
@@ -185,6 +184,14 @@ bool BootService::loadBootStats(std::shared_ptr<FilesystemAdapter> filesystem, B
185184
} else {
186185
success = false;
187186
}
187+
188+
const char *microOcppVersionIn = (*json)["MicroOcppVersion"] | (const char*)nullptr;
189+
if (microOcppVersionIn) {
190+
auto ret = snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", microOcppVersionIn);
191+
if (ret < 0 || (size_t)ret >= sizeof(bstats.microOcppVersion)) {
192+
success = false;
193+
}
194+
} //else: version specifier can be missing after upgrade from pre 1.2.0 version
188195
} else {
189196
success = false;
190197
}
@@ -201,15 +208,58 @@ bool BootService::loadBootStats(std::shared_ptr<FilesystemAdapter> filesystem, B
201208
}
202209
}
203210

204-
bool BootService::storeBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats bstats) {
211+
bool BootService::storeBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats) {
205212
if (!filesystem) {
206213
return false;
207214
}
208215

209-
auto json = initJsonDoc("v16.Boot.BootService", JSON_OBJECT_SIZE(2));
216+
auto json = initJsonDoc("v16.Boot.BootService", JSON_OBJECT_SIZE(3));
210217

211218
json["bootNr"] = bstats.bootNr;
212219
json["lastSuccess"] = bstats.lastBootSuccess;
220+
json["MicroOcppVersion"] = (const char*)bstats.microOcppVersion;
213221

214222
return FilesystemUtils::storeJson(filesystem, MO_FILENAME_PREFIX "bootstats.jsn", json);
215223
}
224+
225+
bool BootService::recover(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats) {
226+
if (!filesystem) {
227+
return false;
228+
}
229+
230+
bool success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool {
231+
return !strncmp(fname, "sd", strlen("sd")) ||
232+
!strncmp(fname, "tx", strlen("tx")) ||
233+
!strncmp(fname, "sc-", strlen("sc-")) ||
234+
!strncmp(fname, "reservation", strlen("reservation")) ||
235+
!strncmp(fname, "client-state", strlen("client-state"));
236+
});
237+
MO_DBG_ERR("clear local state files (recovery): %s", success ? "success" : "not completed");
238+
239+
return success;
240+
}
241+
242+
bool BootService::migrate(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats) {
243+
if (!filesystem) {
244+
return false;
245+
}
246+
247+
bool success = true;
248+
249+
if (strcmp(bstats.microOcppVersion, MO_VERSION)) {
250+
MO_DBG_INFO("migrate persistent storage to MO v" MO_VERSION);
251+
success = FilesystemUtils::remove_if(filesystem, [] (const char *fname) -> bool {
252+
return !strncmp(fname, "sd", strlen("sd")) ||
253+
!strncmp(fname, "tx", strlen("tx")) ||
254+
!strncmp(fname, "op", strlen("op")) ||
255+
!strncmp(fname, "sc-", strlen("sc-")) ||
256+
!strcmp(fname, "client-state.cnf") ||
257+
!strcmp(fname, "arduino-ocpp.cnf") ||
258+
!strcmp(fname, "ocpp-creds.jsn");
259+
});
260+
261+
snprintf(bstats.microOcppVersion, sizeof(bstats.microOcppVersion), "%s", MO_VERSION);
262+
MO_DBG_DEBUG("clear local state files (migration): %s", success ? "success" : "not completed");
263+
}
264+
return success;
265+
}

src/MicroOcpp/Model/Boot/BootService.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,23 @@
1313

1414
#define MO_BOOT_INTERVAL_DEFAULT 60
1515

16+
#ifndef MO_BOOTSTATS_LONGTIME_MS
17+
#define MO_BOOTSTATS_LONGTIME_MS 180 * 1000
18+
#endif
19+
1620
namespace MicroOcpp {
1721

22+
#define MO_BOOTSTATS_VERSION_SIZE 10
23+
1824
struct BootStats {
1925
uint16_t bootNr = 0;
2026
uint16_t lastBootSuccess = 0;
2127

2228
uint16_t getBootFailureCount() {
2329
return bootNr - lastBootSuccess;
2430
}
31+
32+
char microOcppVersion [MO_BOOTSTATS_VERSION_SIZE] = {'\0'};
2533
};
2634

2735
enum class RegistrationStatus {
@@ -79,8 +87,12 @@ class BootService : public MemoryManaged {
7987
void notifyRegistrationStatus(RegistrationStatus status);
8088
void setRetryInterval(unsigned long interval);
8189

82-
static bool loadBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& out);
83-
static bool storeBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats bstats);
90+
static bool loadBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats);
91+
static bool storeBootStats(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats);
92+
93+
static bool recover(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats); //delete all persistent files which could lead to a crash
94+
95+
static bool migrate(std::shared_ptr<FilesystemAdapter> filesystem, BootStats& bstats); //migrate persistent storage if running on a new MO version
8496
};
8597

8698
}

tests/Boot.cpp

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <MicroOcpp/Operations/BootNotification.h>
1313
#include <MicroOcpp/Operations/StatusNotification.h>
1414
#include <MicroOcpp/Operations/CustomOperation.h>
15+
#include <MicroOcpp/Model/Boot/BootService.h>
1516
#include <MicroOcpp/Model/Transactions/TransactionStore.h>
1617
#include <MicroOcpp/Debug.h>
1718
#include <catch2/catch.hpp>
@@ -309,5 +310,121 @@ TEST_CASE( "Boot Behavior" ) {
309310

310311
}
311312

313+
SECTION("Auto recovery") {
314+
315+
//start transaction which will persist a few boot cycles, but then will be wiped by auto recovery
316+
loop();
317+
beginTransaction("mIdTag");
318+
loop();
319+
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
320+
321+
declareConfiguration<const char*>("keepConfigOverRecovery", "originalVal");
322+
configuration_save();
323+
324+
mocpp_deinitialize();
325+
326+
//MO has 2 unexpected power cycles. Probably just back luck - keep the local state and configuration
327+
328+
//Increase the power cycle counter manually because it's not possible to interrupt the MO lifecycle during unit tests
329+
BootStats bootstats;
330+
BootService::loadBootStats(filesystem, bootstats);
331+
bootstats.bootNr += 2;
332+
BootService::storeBootStats(filesystem, bootstats);
333+
334+
mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true);
335+
BootService::loadBootStats(filesystem, bootstats);
336+
REQUIRE( bootstats.getBootFailureCount() == 2 + 1 ); //two boot failures have been measured, +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier
337+
338+
loop();
339+
340+
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
341+
342+
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") );
343+
344+
//check that the power cycle counter has been updated properly after the controller has been running stable over a long time
345+
mtime += MO_BOOTSTATS_LONGTIME_MS;
346+
loop();
347+
BootService::loadBootStats(filesystem, bootstats);
348+
REQUIRE( bootstats.getBootFailureCount() == 0 );
349+
350+
mocpp_deinitialize();
351+
352+
//MO has 10 power cycles without running for at least 3 minutes and wipes the local state, but keeps the configuration
353+
354+
BootStats bootstats2;
355+
BootService::loadBootStats(filesystem, bootstats2);
356+
bootstats2.bootNr += 10;
357+
BootService::storeBootStats(filesystem, bootstats2);
358+
359+
mocpp_initialize(loopback, ChargerCredentials(), filesystem, /*enable auto recovery*/ true);
360+
361+
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverRecovery", "otherVal")->getString(), "originalVal") );
362+
BootStats bootstats3;
363+
BootService::loadBootStats(filesystem, bootstats3);
364+
REQUIRE( bootstats3.getBootFailureCount() == 0 + 1 ); //failure count is reset, but +1 because each power cycle is counted as potentially failing until reaching the long runtime barrier
365+
366+
loop();
367+
REQUIRE( getChargePointStatus() == ChargePointStatus_Available );
368+
369+
}
370+
371+
SECTION("Migration") {
372+
373+
//migration removes files from previous MO versions which were running on the controller. This includes the
374+
//transaction cache, but configs are preserved
375+
376+
auto old_opstore = filesystem->open(MO_FILENAME_PREFIX "opstore.jsn", "w"); //the opstore has been removed in MO v1.2.0
377+
old_opstore->write("example content", sizeof("example content") - 1);
378+
old_opstore.reset(); //flushes the file
379+
380+
loop();
381+
auto tx = beginTransaction("mIdTag"); //tx store will also be removed
382+
auto txNr = tx->getTxNr(); //remember this for later usage
383+
tx.reset(); //reset this smart pointer
384+
loop();
385+
REQUIRE( getChargePointStatus() == ChargePointStatus_Charging );
386+
endTransaction();
387+
loop();
388+
389+
REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) != nullptr ); //tx exists on flash
390+
391+
declareConfiguration<const char*>("keepConfigOverMigration", "originalVal"); //migration keeps configs
392+
configuration_save();
393+
394+
mocpp_deinitialize();
395+
396+
//After a FW update, the tracked version number has changed
397+
BootStats bootstats;
398+
BootService::loadBootStats(filesystem, bootstats);
399+
snprintf(bootstats.microOcppVersion, sizeof(bootstats.microOcppVersion), "oldFwVers");
400+
BootService::storeBootStats(filesystem, bootstats);
401+
402+
mocpp_initialize(loopback, ChargerCredentials(), filesystem); //MO migrates here
403+
404+
size_t msize = 0;
405+
REQUIRE( filesystem->stat(MO_FILENAME_PREFIX "opstore.jsn", &msize) != 0 ); //opstore has been removed
406+
407+
REQUIRE( getOcppContext()->getModel().getTransactionStore()->getTransaction(1, txNr) == nullptr ); //tx history entry has been removed
408+
409+
REQUIRE( !strcmp(declareConfiguration<const char*>("keepConfigOverMigration", "otherVal")->getString(), "originalVal") ); //config has been preserved
410+
}
411+
412+
SECTION("Clean unused configs") {
413+
414+
declareConfiguration<const char*>("neverDeclaredInsideMO", "originalVal"); //unused configs will be cleared automatically after the controller has been running for a long time
415+
configuration_save();
416+
417+
mocpp_deinitialize();
418+
419+
mocpp_initialize(loopback, ChargerCredentials(), filesystem); //all configs are loaded here, including the test config of this section
420+
loop();
421+
422+
//unused configs will be cleared automatically after long time
423+
mtime += MO_BOOTSTATS_LONGTIME_MS;
424+
loop();
425+
426+
REQUIRE( !strcmp(declareConfiguration<const char*>("neverDeclaredInsideMO", "newVal")->getString(), "newVal") ); //config has been removed
427+
}
428+
312429
mocpp_deinitialize();
313430
}

0 commit comments

Comments
 (0)