Repository navigation
Conversation
GuildHandler caught the IOException from its own guild load, logged it and returned with an empty guild map, so the catch in Guilds.onEnable() never fired. The server came up as if the data had loaded and the scheduled save then wrote that empty map back over the guilds the plugin had failed to read. The constructor now lets the failure out, and onEnable loads the guild data in its own try/catch that stops the plugin with a message naming what could not be read. The handler stays null in that case, so nothing is saved. A single guild that cannot be set up is handled per record, the way the tier fallback already handles a guild with an unknown tier: that guild is left out of the running plugin and the other guilds carry on. Its file is not written, because the save only writes the guilds that are in memory. GuildJsonProvider remembers the files it could not read and refuses to write to them or delete them, since such a file is the only copy of its guild. A data folder that cannot be listed now raises an IOException naming the folder instead of a NullPointerException that named nothing.
This was referenced Oct 22, 2026
added 6 commits
October 22, 2026 12:27
A guild file that does not parse is the only copy of that guild. The plugin now copies it into the backup folder before it carries on without it, so that there is a second copy somewhere the plugin will never write to. The original stays exactly where the operator put it, and it keeps being protected from writes and deletions. A folder where no guild file can be read is not an empty guild database. It is a guild database the plugin failed to read, and a server that comes up as if it had no guilds hides that. The load fails there now, the way it already failed for a folder that cannot be listed, and the plugin stops with a message that says what could not be read.
The totals were contended between the open branches. The header now says what the merged suite will have, the invite code section carries the rows for the tests the other branches add, and the startup section carries the count of the tests in this branch's spec.
…e others The guild provider now takes its folders in the order the other three providers use, and keeps them in the fields in the same order, so a merge cannot leave the two folders the wrong way round. Both of them are files, and the compiler says nothing about a swap. The provider also uses the shared helpers for listing the record files and for backing a file up, so all four storages call the backup in the same place and the merged tree has one way of doing it. The constructor warns when the plugin's data folder is not the parent of the record folder, naming both, because that is the one mistake the compiler cannot catch. The test file is now the superset of this branch's cases and the JSON storage branch's, so either side of that conflict keeps every case.
The plugin's data folder and the record folder are both files, so a caller can pass them the wrong way round without the compiler noticing. The provider warns about it now, and this covers the warning.
The totals were counted before the smoke suite grew its recovery checks, so the header was two short. The section headings now carry the counts the merged suite will have, and the sections another branch owns are placeholders here rather than copies of their tables, so the merge is a union instead of a conflict.
…er storages The JSON storage branch put the warning about a swapped pair of folders into the shared helper, and reports a delete that did not happen. Both belong to all four storages, so the guild provider uses them too. The test file is now the superset of that branch's file and this one's: every case it has, plus the ones about a load where nothing could be read and about the guilds that survived a save and a restart.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
GuildHandler's constructor loaded the guilds and caught theIOExceptionitself:The handler returned normally with an empty or half-filled guild map, so the
catch (IOException e)inGuilds.onEnable()never fired. The plugin came up as if the data had loaded, and the scheduled save then wrote that map back over the guilds it had failed to read.A unit test against the current code reproduces it: a
GuildAdapterwhosegetAllGuilds()throws leaves the handler constructed with zero guilds and nothing thrown.The per-file case behaved differently.
GuildJsonProvider.getAllGuilds()catches a parse error per file, logs it and skips that guild, which is the right call for one bad file. Two things were still missing:updateGuildon that id replaced its contents with a guild the plugin had made up.I checked what the ordinary save path does to a file that failed to parse: nothing.
GuildHandler.saveData()writesguilds.values(), and the guild that failed to parse is not in that map, so the file stayed on disk. The exposure was a write reaching the same path by another route, and there was no guard against it.A third case: the guild data folder could not be listed.
listFiles()returns null there, andObjects.requireNonNullturned that into aNullPointerExceptionthat named nothing.A fourth case turned up while the first three were being fixed. The provider skips a file it cannot parse and returns the guilds it could read. When every file in the folder was one of those, the provider returned an empty list, and the plugin read that as a server with no guilds. The guilds were still on disk, and the first save wrote over them. The plugin had no way to tell an empty guild database from one it had failed to read.
A file the plugin cannot read also had no second copy anywhere. The file was left where it was and the plugin refused to write to it, but that file was the only copy of that guild.
The fix
GuildHandler's constructor now declaresthrows IOExceptionand lets the failure out.Guilds.onEnable()loads the guild data in its own try/catch, so a failed read stops the plugin with a message that says what could not be read and that the guild files have not been changed. The handler is left null in that case, soonDisable()saves nothing.One guild that cannot be set up is handled per record, the way the tier fallback already handles a guild with an unknown tier: that guild is left out of the running plugin with a message naming it, and the other guilds carry on. Its file is not written, because the save only writes the guilds that are in memory. If no guild at all can be set up, that is a broken setup rather than one broken guild, and the plugin stops.
GuildJsonProviderremembers the files it could not read and refuses to write to them or delete them. The refusal is anIOException, soGuildAdapter.saveGuilds()logs it per record and saves the rest. A folder that cannot be listed now raises anIOExceptionnaming the folder, in the load path and inguildExists(), so the save refuses to write into a folder it cannot see.Every file the plugin cannot read is now copied into
plugins/Guilds/backups/corrupt/<timestamp>/guilds/before the plugin carries on without it, and the log says where the copy went. The copy is byte for byte, the original stays where the operator put it, and the timestamp comes from the file's own last-modified time, so reading the same corruption again finds the copy that is already there instead of making another one. The backup folder sits outside every folder the plugin reads, so a backup is never loaded as a guild.A folder where no guild file can be read is now a load failure. The provider throws, the handler passes the failure out, and the plugin stops with the message it already used for a guild data read failure. A folder with no files in it is still a server with no guilds, and still starts.
The provider uses the shared
JsonFileUtilshelpers that the other three storages use, so all four of them list their record files the same way, back a file up in the same place and warn about the same mistake. Its folders are in the same order as theirs, the record folder first, because both of them are files and the compiler says nothing about a swap.JsonFileUtils.warnIfFoldersAreSwappednames both folders and says that the copies will not land where they should, so a swap is caught at startup rather than quietly reading the wrong directory.Tests
src/test/java/me/glaremasters/guilds/database/guild/provider/GuildJsonProviderTest.javacovers the provider against a temp data folder. It is the superset of this branch's cases and the JSON storage branch's: which files count as guilds, that a file that cannot be parsed or that carries no id is skipped, left on disk, protected from being written to or deleted, and copied aside byte for byte, that the copy is outside the data folder and keeps the file name, that two corrupt files with the same name do not overwrite each other, that the same corruption is copied once rather than once per read, that a backup which cannot be written is reported while the original file stays as it was, that a delete which did not happen is reported rather than swallowed, that a folder where no file can be read is a load failure, that a folder with no files in it is not, that the guilds which could be read are still there after a save and after a restart, and that a provider given its two folders the wrong way round says so.src/test/java/me/glaremasters/guilds/guild/GuildHandlerLoadTest.javacovers the handler: a failed read is thrown to the caller instead of being logged and swallowed, a successful load still loads the guilds, one guild that cannot be set up does not cost the server the others, no usable guild at all stops the plugin, and a load where every guild file is unreadable stops the plugin too.src/test/e2e/tests/startup-loading.spec.tscovers the startup path on a real server. The E2E environment stages two guild files into the plugin's data folder: one that reads and one that does not parse. The spec asserts that the plugin enables, does not disable itself, names the unreadable file in the log, lists the guild that read, does not list the guild that did not, leaves both files on disk, copies the unreadable file into the backup folder, and keeps that copy outside the data folder.docs/e2e-coverage.mdlists the spec and the totals the merged suite will have.Results
The four end-to-end tests are the last four in the run:
Compatibility
No Bukkit API changed, no config key changed, and the stored file format is untouched. The constructor of
GuildHandlernow declaresthrows IOException; the only caller isGuilds.onEnable(), and the plugin's public API isGuildsAPI.GuildJsonProvidergained a second constructor that takes the plugin's data folder, so that the backups land beside the rest of the plugin's data; the constructor that takes only the data folder still works and takes the backup folder from its parent. The two folders are in the order the other three storages use them in, the record folder first.The legacy probe passes: this change references no
Materialconstant. The Java 11 bytecode target is unchanged.Limitations
Guilds.onEnable()itself is not unit-testable. It needs a running server with Vault, an economy provider, a permissions provider and the plugin manager, none of which exist in a plain JVM test. The load failure is therefore covered at theGuildHandlerboundary, and the plugin lifecycle behaviour is covered by the E2E spec.A guild whose file parses but holds unusable data, a member without a role for instance, is left out of the running plugin and its file is left as it is. That file is not copied into the backup folder. The variant of the same case where the record carries no id at all is skipped, protected and copied aside.
The autosave and the shutdown save run the same code, so the unit test that runs the save over a folder holding a corrupt file covers both. The end-to-end suite shows the file unchanged after the autosave. The shutdown save runs after the last spec, so nothing in the suite observes it there.
CooldownHandler.loadCooldowns()andChallengeHandler.loadChallenges()still swallow anIOExceptionthe wayGuildHandlerdid. They are outside this workstream, and cooldowns are timers rather than records, but the pattern is the same and is worth a follow-up.Merging this with #846
#846 (the JSON storage filtering workstream) rewrites the same file,
GuildJsonProvider.java, and adds a test file at the same path,GuildJsonProviderTest.java, so the two branches conflict on both. The two changes are complementary rather than contradictory: #846 enumerates only.jsonfiles as records and skips a record that carries no id, and this branch protects a file the plugin could not read from being written to or deleted, refuses to start when nothing at all could be read, and copies that file aside.This branch follows #846's shape rather than carrying a second one:
DATA_TYPE, the name fix: only read .json files as records in the JSON storage #846 usesJsonFileUtils.backupCorruptFile(...),JsonFileUtils.listDataFiles(...)andJsonFileUtils.warnIfFoldersAreSwapped(...), so all four storages back a file up in the same place, enumerate their records the same way and complain about the same mistakeJsonFileUtils, and the test helpersLogCapture,TestGson,CorruptBackupsandUnwritableFolders, are on this branch as byte for byte copies of fix: only read .json files as records in the JSON storage #846's, so those files merge without a conflictWhat this branch adds to that shape is the rule that a folder where no guild file can be read is a load failure, and the count of how many files could not be read.
The two test files are an add/add conflict. This branch's file is the superset: it carries every case from #846's file, plus the cases about the load failure, about the load not writing anything, and about the guilds that survived a save and a restart. Taking either side of that conflict and adding the other's cases gives the merged file every case from both branches.
I merged #846 into a scratch copy of this branch and ran the suite there. The tree compiles, 162 tests pass with no failures, and the case that matters most is still there: a folder holding one corrupt
.jsonfile and nothing else is a load failure, not an empty guild database.The branches are meant to stay independent and to target
masteron their own. Nothing here needs #846, and nothing here has been rebased onto it.