Arkanjo — Improving Cache Compatibility Across Versions
Arkanjo Contribution
Introduction
As part of the Free and Open Source Software Development (MAC0470) course, I contributed to Arkanjo, an open-source plagiarism detection tool written in C++.
Unlike my Linux kernel contribution, which focused on modernizing existing code, this contribution involved implementing a new feature to improve the reliability of Arkanjo’s preprocessing cache.
The work was split into two pull requests. The first introduced version metadata into the cache, allowing the application to know which version generated the preprocessing data. The second built upon that information by verifying cache compatibility before reuse and warning users whenever an outdated cache might produce unreliable results.
During the review process, the implementation also evolved to include a small architectural refactoring that reduced unnecessary dependencies between project modules.
The Problem
Arkanjo performs a preprocessing step before comparing source files. Since preprocessing can be computationally expensive, the generated data is stored on disk so it can be reused in future executions.
Originally, however, the cache only stored information such as the preprocessing directory and execution timestamp.
1
2
3
4
struct PreprocessRunParams {
fs::path path;
std::string finished_time;
};
This worked well as long as the preprocessing format remained unchanged. However, after updating Arkanjo, the application had no way to determine whether an existing cache had been generated by an older version.
As a result, users could unknowingly reuse outdated preprocessing data, potentially leading to incorrect similarity results without receiving any indication that something was wrong.
The first step was therefore to extend the cache metadata.
1
2
3
4
5
struct PreprocessRunParams {
fs::path path;
std::string finished_time;
std::string version;
};
Although this modification seems small, it provides the foundation necessary for future compatibility checks.
Exposing the Project Version
Once the cache was capable of storing version information, the next challenge was making the current project version available inside the C++ source code.
The project already defined its version through CMake, so the solution was to expose it as a compile-time definition.
1
2
3
4
target_compile_definitions(project_options INTERFACE
THIRD_PARTY_DIR="${CMAKE_INSTALL_FULL_LIBDIR}/arkanjo/third-party"
PROJECT_VERSION="${PROJECT_VERSION}"
)
With this definition available, the preprocessing stage could simply persist the version together with the remaining metadata.
1
2
3
4
5
6
7
std::string version = PROJECT_VERSION;
json data = PreprocessRunParams{
path,
time_str,
version
};
Every cache generated by Arkanjo now carries information about the version that produced it.
Checking Cache Compatibility
Once version information became available, the application could verify whether a cache should still be considered valid before using it.
A new function was introduced for this purpose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool PreprocessState::is_cache_compatible(
const std::string& cache_version)
{
int current_major, current_minor, current_patch;
int cache_major, cache_minor, cache_patch;
char dot;
std::istringstream(PROJECT_VERSION)
>> current_major >> dot
>> current_minor >> dot
>> current_patch;
std::istringstream(cache_version)
>> cache_major >> dot
>> cache_minor >> dot
>> cache_patch;
return current_major == cache_major &&
current_minor == cache_minor;
}
Rather than comparing the version strings directly, the implementation parses the semantic version numbers and compares their individual components.
This makes the comparison more explicit and also leaves room for future compatibility policies should the project decide to support more flexible version ranges.
Warning the User
The compatibility check is performed before the cached similarity table is loaded.
1
2
3
4
5
6
7
8
9
10
11
auto params = PreprocessState::read_current_run_params();
if (!PreprocessState::is_cache_compatible(params.version)) {
FormatterManager::warn(
"Cache generated with Arkanjo v" +
params.version +
" may be incompatible with the current version v" +
std::string(PROJECT_VERSION) +
". Results may be inaccurate."
);
}
Previously, Arkanjo would simply load any cache that existed.
Now, users receive an explicit warning whenever cached data originates from an incompatible version, making the application’s behavior considerably safer while still allowing advanced users to proceed if they choose.
An Architectural Improvement
While implementing this feature, an architectural issue became apparent.
The compatibility verification needed access to preprocessing metadata, but that functionality lived inside the commands module, while the cache itself belonged to the base module.
This resulted in the following dependency direction:
1
2
3
4
5
6
7
commands
└── Preprocessor
base
└── SimilarityTable
↑
depends on commands
During code review, one of the maintainers pointed out this unnecessary dependency:
While reviewing this and a few other parts of ArKanjo, I noticed that we have dependencies in the following direction:
commands → base
Ideally, this dependency shouldn’t exist.
Would it make sense to move Preprocessor into base before merging this feature? We could rename it to
PreprocessState, which contains the methods:
save_current_run_params()read_current_run_params()is_cache_compatible()
I agreed with the suggestion and refactored the implementation accordingly.
Instead of leaving preprocessing state management inside a command implementation, those responsibilities were moved into a dedicated PreprocessState component.
The resulting organization became much cleaner.
1
2
3
4
5
6
base
├── SimilarityTable
└── PreprocessState
commands
└── uses PreprocessState
Besides solving the dependency issue, this separation also better reflects the responsibility of each component: command execution remains in the command layer, while persistent preprocessing metadata is managed independently.
Development and Review Process
Although the feature itself was relatively small, implementing it required touching several independent parts of the project.
The changes involved modifying the cache format, integrating the build system with the source code through CMake, updating serialization routines, implementing compatibility verification, and finally refactoring part of the project’s architecture after the review discussion.
One aspect I particularly appreciated was that the review process went beyond identifying bugs. The maintainers also discussed design decisions and module organization, resulting in an implementation that was cleaner than my initial proposal.
Experiencing this type of review reinforced that contributing to open-source software is not only about making a feature work, but also about improving the overall maintainability of the project.
What I Learned
This contribution highlighted an important lesson about software that stores persistent data: whenever a file format may evolve over time, version metadata becomes essential.
Without it, applications have no reliable way to determine whether previously generated data is still compatible with the current implementation.
Another interesting takeaway was seeing how feature development often uncovers architectural improvements. While implementing cache versioning, the need to access preprocessing metadata exposed an unnecessary dependency between modules. Addressing that issue ultimately produced a cleaner design than the one originally proposed.
Finally, this contribution provided another opportunity to participate in the workflow of a real open-source project, from implementing a feature to discussing architecture and refining the design through code review.
Conclusion
Although the implementation itself was relatively compact, it significantly improved the robustness of Arkanjo’s preprocessing pipeline.
By associating cache files with the version that generated them and validating compatibility before reuse, the application is now able to detect potentially outdated caches and warn users before inaccurate results occur.
Equally important, the contribution also improved the internal organization of the preprocessing code, reducing module coupling and making future maintenance easier.
Overall, this experience reinforced that even relatively small features can have a meaningful impact when they improve both the user experience and the long-term maintainability of an open-source project.