Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Part of #267: Implement in Agora version upgrader #2364

Draft
wants to merge 1 commit into
base: v0.x.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@
[submodule "serialization"]
path = submodules/serialization
url = https://github.com/bosagora/serialization.git
[submodule "semver"]
path = submodules/semver
url = https://github.com/dcarp/semver.git
1 change: 1 addition & 0 deletions dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"libsodiumd": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] },
"localrest": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] },
"ocean": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] },
"semver": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] },
"serialization": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] },
"vibe-d": { "version": "*", "dflags" : [ "-preview=in", "-revert=dtorfields" ] }
}
Expand Down
1 change: 1 addition & 0 deletions dub.selections.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"mir-linux-kernel": {"path":"submodules/mir-linux-kernel/"},
"ocean": {"path":"submodules/ocean/"},
"openssl": {"path":"submodules/openssl/"},
"semver": {"path":"submodules/semver/"},
"serialization": {"path":"submodules/serialization/"},
"stdx-allocator": {"path":"submodules/stdx-allocator/"},
"taggedalgebraic": {"path":"submodules/taggedalgebraic/"},
Expand Down
15 changes: 13 additions & 2 deletions source/agora/node/FullNode.d
Original file line number Diff line number Diff line change
Expand Up @@ -246,17 +246,29 @@ public class FullNode : API
import std.datetime.systime : Clock, SysTime, unixTimeToStdTime;
import std.datetime.timezone: UTC;

enum build_version = import(VersionFileName);

this.config = config;
this.log = this.makeLogger();
this.params = FullNode.makeConsensusParams(config);

this.stateDB = this.makeStateDB();
this.cacheDB = this.makeCacheDB();
this.storage = this.makeBlockStorage();

// We need to apply updates early on - At the very least,
// before the Ledger is instantiated.
// It would be better if this could be moved out of the ctor,
// but that would require delaying most of the initialization
// to another function.
import agora.node.Versioning;
applyVersionDifferences(this.stateDB, this.cacheDB, this.storage,
build_version, this.config, this.log);

this.taskman = this.makeTaskManager();
this.clock = this.makeClock(this.taskman);
this.metadata = this.makeMetadata();
this.network = this.makeNetworkManager(this.metadata, this.taskman, this.clock);
this.storage = this.makeBlockStorage();
this.fee_man = this.makeFeeManager();
this.utxo_set = this.makeUTXOSet();
this.pool = this.makeTransactionPool();
Expand Down Expand Up @@ -300,7 +312,6 @@ public class FullNode : API
Utils.getCollectorRegistry().addCollector(&this.collectBlockStats);
Utils.getCollectorRegistry().addCollector(&this.collectValidatorStats);

enum build_version = import(VersionFileName);
this.app_stats.setMetricTo!"agora_application_info"(
1, // Unused, see article linked in the struct's documentationx
build_version,
Expand Down
160 changes: 160 additions & 0 deletions source/agora/node/Versioning.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*******************************************************************************

Apply version upgrades to a node

To allow for a smooth upgrade path for Agora node, there is a built-in
upgrade system: When a node starts, it checks a specific table in stateDB,
and gradually run upgrade scripts as needed.

Copyright:
Copyright (c) 2019-2021 BOSAGORA Foundation
All rights reserved.

License:
MIT License. See LICENSE for details.

*******************************************************************************/

module agora.node.Versioning;

import agora.common.Config;
import agora.common.Ensure;
import agora.common.ManagedDatabase;
import agora.node.BlockStorage;
import agora.utils.Log;

import semver;

import std.path;

/*******************************************************************************

Apply all required version upgrades to the state

This is called from the FullNode's constructor after all the state handling
members (`stateDB`, `cacheDB`, block `storage`) have been initialized.

If any upgrade is needed at all (the version stored in the metadata
table is different from the one we're running), we apply the required
fix(es). This process can also handle downgrades.

The logger is also initialized so we can provide feedback to the user.
The config has already been parsed, so any fix needed would need to
happen in two steps (one version allowing the new syntax and doing the
fix, then a new version rejecting the old syntax).

Params:
stateDB = The node's `stateDB`, holding the blockchain state
cacheDB = The node's `cacheDB`, holding the node's cached data
storage = The node's block storage, holding known blocks
current = The version we are currently running, as read from
the version file
config = The parsed configuration
log = Logger to output any message to, if any

Throws:
If an error happened during the upgrade, as we don't want to continue
and throw random errors to the user in a seemingly unrelated place.

***************************************************************************/

package void applyVersionDifferences (
ManagedDatabase stateDB, ManagedDatabase cacheDB, IBlockStorage storage,
string current, in Config config, Logger log)
{
// We currently use this in a few places, allow for a transition period
if (current == "HEAD")
{
log.info("Current version is set to HEAD - cannot check for upgrades");
return;
}

void printFatalMessages ()
{
log.fatal("Cannot continue initialization - State DB is in an inconsistent state");
log.fatal("Please fix your installation if possible, or remove {} ",
config.node.data_dir.buildPath("state.db"));
log.fatal("This will rebuild your blockchain state from scratch, and may take some time");
log.fatal("It is recommended to also remove the cache DB at {} if the state DB is removed",
config.node.data_dir.buildPath("state.db"));
log.fatal("If you believe this is an issue with Agora, please report an issue at " ~
"https://github.com/bosagora/agora/");
}

const vers = SemVer(current);
ensure(vers.isValid, "Version '{}' is not a valid version", current);

const exists = stateDB.execute(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='metadata'")
.oneValue!int;

// TODO: Remove once v0.17.0 has been deployed and all nodes have a metadata table
version (none)
{
if (!exists)
{
stateDB.execute("CREATE TABLE metadata (key TEXT UNIQUE NOT NULL, value TEXT NOT NULL)");
stateDB.setMetadataVersion(current);
return;
}
}
else
{
if (!exists)
{
// If the `metadata` table doesn't exists, it means either the DB
// is compromised or this is the first run - Let's check if it's the later.
const tables = stateDB.execute(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table'")
.oneValue!int;

if (tables == 0)
{
// Create it and return, since there is no existing state
stateDB.execute("CREATE TABLE metadata (key TEXT UNIQUE NOT NULL, value TEXT NOT NULL)");
stateDB.setMetadataVersion(current);
return;
}

log.fatal("No 'metadata' table exists in stateDB, but stateDB has {} tables!", tables);
printFatalMessages();
ensure(false, "Could not determine previous state of the node - Check logs for more infos");
}
}

auto results = stateDB.execute("SELECT value FROM metadata WHERE key='version'");
if (results.empty())
{
printFatalMessages();
ensure(false, "Could not find version information in metadata table - Check logs for more infos");
}
const oldVersStr = results.oneValue!string;
const oldVers = SemVer(oldVersStr);
if (!oldVers.isValid())
{
printFatalMessages();
ensure(false, "Version stored in metadata ({}) is not a valid version - " ~
"Check logs for more infos", oldVersStr);
}

// Most common case, do not output any message
if (oldVers == vers) return;

if (oldVers < vers)
{
const size_t upgrades = 1;
log.info("Need to apply {} upgrades from {} to {}", upgrades, oldVers, vers);
}
else
{
const size_t downgrades = 1;
log.info("Need to apply {} downgrades from {} to {}", downgrades, oldVers, vers);
}
stateDB.setMetadataVersion(current);
}

/// Set the current version in the metadata
private void setMetadataVersion (ManagedDatabase stateDB, string version_)
{
stateDB.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('version', ?)", version_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sqlite have this

}
1 change: 1 addition & 0 deletions submodules/semver
Submodule semver added at 101204