diff --git a/foundation-store-contracts/gradle/owned-output-patterns.txt b/foundation-store-contracts/gradle/owned-output-patterns.txt index 171714276bb..78d129a0bb4 100644 --- a/foundation-store-contracts/gradle/owned-output-patterns.txt +++ b/foundation-store-contracts/gradle/owned-output-patterns.txt @@ -3,4 +3,5 @@ network/crypta/store/StorableBlock* network/crypta/store/BlockMetadata* network/crypta/store/GetPubkey* - +network/crypta/node/stats/StoreAccessStats* +network/crypta/node/stats/StatsNotAvailableException* diff --git a/src/main/java/network/crypta/node/stats/StatsNotAvailableException.java b/foundation-store-contracts/src/main/java/network/crypta/node/stats/StatsNotAvailableException.java similarity index 96% rename from src/main/java/network/crypta/node/stats/StatsNotAvailableException.java rename to foundation-store-contracts/src/main/java/network/crypta/node/stats/StatsNotAvailableException.java index a0517c81883..3a70e222c2a 100644 --- a/src/main/java/network/crypta/node/stats/StatsNotAvailableException.java +++ b/foundation-store-contracts/src/main/java/network/crypta/node/stats/StatsNotAvailableException.java @@ -25,13 +25,12 @@ *
  • Plugin-provided statistics while the plugin is missing, disabled, or reloading. * * - * @see network.crypta.node.stats.DataStoreStats - * @see network.crypta.node.stats.StoreAccessStats * @author nikotyan */ @SuppressWarnings("unused") public class StatsNotAvailableException extends Exception { + /** Serialization identifier for stable exception transport and persistence compatibility. */ @Serial private static final long serialVersionUID = -7349859507599514672L; /** @@ -65,7 +64,7 @@ public StatsNotAvailableException(String s) { * and logging. * * @param s detail message describing the high-level condition; may be {@code null} if the cause - * is sufficiently descriptive on its own. + * is descriptive enough on its own. * @param throwable the underlying cause that prevented providing statistics; may be {@code null} * if unknown or not applicable. */ diff --git a/src/main/java/network/crypta/node/stats/StoreAccessStats.java b/foundation-store-contracts/src/main/java/network/crypta/node/stats/StoreAccessStats.java similarity index 88% rename from src/main/java/network/crypta/node/stats/StoreAccessStats.java rename to foundation-store-contracts/src/main/java/network/crypta/node/stats/StoreAccessStats.java index 2d073e3c8be..7dd3359e09b 100644 --- a/src/main/java/network/crypta/node/stats/StoreAccessStats.java +++ b/foundation-store-contracts/src/main/java/network/crypta/node/stats/StoreAccessStats.java @@ -6,7 +6,7 @@ *

    Instances of this abstract type expose read-oriented counters (hits, misses, false positives) * and write counters as observed by a particular store. Implementations typically back these values * with thread-safe counters updated by the store’s I/O path and may compute derived rates from the - * exposed primitives. The contract is deliberately minimal so it can model both in-memory caches + * exposed primitives. The contract is deliberately minimal, so it can model both in-memory caches * and persistent stores with different eviction and validation strategies. * *

    The methods are intended for metrics dashboards, admin endpoints, and diagnostics. Values @@ -18,10 +18,9 @@ *

    * - * @see network.crypta.node.stats.DataStoreStats * @see network.crypta.node.stats.StatsNotAvailableException */ public abstract class StoreAccessStats { @@ -62,8 +61,8 @@ protected StoreAccessStats() {} * Returns the number of false positives detected during lookups. * *

    False positives generally occur when a preliminary test (such as a probabilistic filter) - * indicates presence, yet the object is not actually retrievable. This counter helps assess - * filter tuning and its impact on unnecessary follow-on work. + * indicates presence, yet the object is not retrievable. This counter helps assess filter tuning + * and its impact on unnecessary follow-on work. * * @return non-negative count of false positives observed so far; monotonically non-decreasing per * process unless counters are reset by the implementation. @@ -73,8 +72,8 @@ protected StoreAccessStats() {} /** * Returns the number of write operations issued to the store. * - *

    Depending on the store, a write may represent an insert, update, or a completed fill after a - * miss. The counter reflects successfully issued writes; failures may or may not be included + *

    Depending on the store, a writing may represent an insert, update, or a completed fill after + * a miss. The counter reflects successfully issued writes; failures may or may not be included * based on implementation policy. * * @return non-negative count of writes observed so far; monotonically non-decreasing per process @@ -111,8 +110,8 @@ public long successfulReads() { * Returns the read success rate as a percentage of total reads. * *

    Calculated as {@code (100.0 * hits() / readRequests())}. When no reads have been observed - * yet, the rate is undefined and this method signals unavailability rather than returning a value - * that could be misinterpreted. + * yet, the rate is undefined, and this method signals unavailability rather than returning a + * value that could be misinterpreted. * * @return a percentage in the range {@code [0.0, 100.0]} when at least one read has been * observed. The exact rounding behavior follows IEEE-754 double arithmetic. @@ -140,7 +139,7 @@ public double accessRate(long nodeUptimeSeconds) { } /** - * Returns the average write rate per second for the node uptime period. + * Returns the average writing rate per second for the node uptime period. * *

    Computed as {@code writes() / nodeUptimeSeconds}. As with {@link #accessRate(long)}, callers * should provide a strictly positive uptime to avoid undefined ratios at startup. diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreAlertSink.java b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreAlertSink.java new file mode 100644 index 00000000000..c589c0099e8 --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreAlertSink.java @@ -0,0 +1,40 @@ +package network.crypta.store.alerts; + +/** + * Receives dynamic store-maintenance alert sources published by store implementations. + * + *

    This interface is the narrow boundary between the leaf-owned store layer and the root-owned + * runtime alert system. Stores do not format text, localize strings, or build UI fragments. + * Instead, they register a live {@link StoreMaintenanceAlertSource} and allow the runtime layer to + * decide whether that source becomes a user alert, log entry, diagnostics panel, or no visible + * output at all. + * + *

    The contract is intentionally small so the store layer can remain reusable and testable. A + * sink implementation may keep the source for repeated polling, transform it into another alert + * representation, or ignore it completely when the current runtime does not expose maintenance + * alerts. + * + * @see StoreMaintenanceAlertSource + */ +public interface StoreAlertSink { + /** + * Sink that silently drops all registrations. + * + *

    Use this when a caller wants to avoid null checks but has no alert destination for store + * maintenance progress. + */ + StoreAlertSink NO_OP = _ -> {}; + + /** + * Registers a store-maintenance alert source with this sink. + * + *

    The source is expected to remain dynamic after registration. Implementations may poll it + * repeatedly, snapshot it immediately, or decide not to surface it. Callers should typically + * register long-lived sources once rather than creating new source objects for every progress + * update. + * + * @param alert live source describing a maintenance operation and its current progress; sink + * implementations may ignore it when the runtime does not expose alerts + */ + void register(StoreMaintenanceAlertSource alert); +} diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertKind.java b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertKind.java new file mode 100644 index 00000000000..6d613bf49f0 --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertKind.java @@ -0,0 +1,26 @@ +package network.crypta.store.alerts; + +/** + * Lists the maintenance progress categories that stores can expose through the alert SPI. + * + *

    Runtime adapters use these values to pick localization keys and presentation details while the + * store layer remains free of user-interface code. The enum is intentionally small because the + * current boundary only needs to describe long-running resize and rebuild work. + */ +public enum StoreMaintenanceAlertKind { + /** + * Progress for a store resize operation that changes the effective capacity of the datastore. + * + *

    Typical runtimes present this as an operator-visible task because resizing can run for an + * extended period and may temporarily reduce performance. + */ + RESIZE_PROGRESS, + + /** + * Progress for a store rebuild or maintenance pass over existing store data. + * + *

    This covers both legacy slot-filter rebuilds after an unclean shutdown and rebuilds that + * convert data into a newer maintenance format. + */ + REBUILD_PROGRESS +} diff --git a/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertSource.java b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertSource.java new file mode 100644 index 00000000000..d55eecf58ab --- /dev/null +++ b/foundation-store-contracts/src/main/java/network/crypta/store/alerts/StoreMaintenanceAlertSource.java @@ -0,0 +1,102 @@ +package network.crypta.store.alerts; + +/** + * Provides live, store-facing data for a maintenance progress alert. + * + *

    Implementations expose a small, polling-friendly view of an in-flight store operation such as + * a resize or rebuild. The values returned from this interface are intentionally presentation-free: + * they identify the affected store, describe the kind of work in progress, and report progress + * counters that the runtime can localize and render however it needs. This keeps formatting, HTML + * generation, and runtime policy outside the store layer. + * + *

    Callers should treat instances as dynamic and read-mostly. Repeated invocations may return + * different values as maintenance proceeds, and {@link #isValid()} may switch from {@code true} to + * {@code false} when the operation completes or is canceled. + * + * @see StoreAlertSink + * @see StoreMaintenanceAlertKind + */ +public interface StoreMaintenanceAlertSource { + /** + * Returns a stable anchor string for this maintenance alert. + * + *

    The anchor is used by runtime adapters as a durable identifier for deduplication, fragment + * links, or UI element IDs. It should remain stable for the lifetime of the underlying + * maintenance task and avoid characters that would be awkward in URLs or HTML identifiers. + * + * @return a short, stable identifier for the alert source that remains constant while the + * maintenance operation is active + */ + String anchor(); + + /** + * Returns the human-readable name of the affected store. + * + *

    This value is intended for interpolation into localized runtime messages. It should be + * concise and recognizable to operators, for example, the configured datastore name or cache + * label. + * + * @return store name suitable for operator-facing alert text and diagnostics + */ + String storeName(); + + /** + * Returns the kind of maintenance task currently represented by this source. + * + *

    Runtime adapters use this value to select the appropriate localization keys, severity, and + * surrounding presentation. Implementations should return the same value throughout one logical + * maintenance task. + * + * @return enum value describing whether the alert represents resize progress, rebuild progress, + * or another future maintenance category + */ + StoreMaintenanceAlertKind kind(); + + /** + * Returns the amount of maintenance work completed so far. + * + *

    The unit must match {@link #total()}. Implementations typically report processed slots, + * entries, or similar work units. The value should be non-negative and usually monotonic while + * the task is valid. + * + * @return completed work units for the current maintenance task, in the same units as {@link + * #total()} + */ + long processed(); + + /** + * Returns the total amount of work expected for the maintenance task. + * + *

    This provides the denominator for progress displays. Implementations should keep the unit + * consistent with {@link #processed()}. The value may be zero when the total is unknown or when a + * task has not started computing progress yet. + * + * @return total expected work units for the current task, using the same unit system as {@link + * #processed()} + */ + long total(); + + /** + * Reports whether the task is rebuilding into the newer slot-filter format. + * + *

    This flag lets the runtime choose between the legacy rebuild wording and the newer + * conversion wording without forcing the store to produce preformatted strings. Callers should + * interpret the flag only for rebuild-style alerts. + * + * @return {@code true} when the rebuild path is converting to the new slot-filter format; {@code + * false} for legacy rebuilds and non-rebuild tasks + */ + boolean newSlotFilter(); + + /** + * Reports whether this alert source should still be shown. + * + *

    Once this method returns {@code false}, runtime adapters should treat the maintenance alert + * as finished or obsolete and stop presenting it. Implementations may return {@code false} when + * the task completes, is canceled, or is replaced by a new source. + * + * @return {@code true} while the source represents an active maintenance condition; {@code false} + * when the alert should be considered stale or complete + */ + boolean isValid(); +} diff --git a/foundation-support/build.gradle.kts b/foundation-support/build.gradle.kts index 18595b36f55..932bcad345c 100644 --- a/foundation-support/build.gradle.kts +++ b/foundation-support/build.gradle.kts @@ -6,9 +6,12 @@ plugins { version = rootProject.version dependencies { + implementation(project(":foundation-fs")) implementation(libs.slf4jApi) implementation(project(":thirdparty-legacy")) implementation(libs.commonsCompress) + implementation(libs.jna) + implementation(libs.jnaPlatform) implementation(files(rootProject.file("libs/wrapper.jar"))) compileOnly(libs.jetbrainsAnnotations) } diff --git a/foundation-support/gradle/owned-output-patterns.txt b/foundation-support/gradle/owned-output-patterns.txt index 2507e7fcf53..779ee962d7a 100644 --- a/foundation-support/gradle/owned-output-patterns.txt +++ b/foundation-support/gradle/owned-output-patterns.txt @@ -2,6 +2,8 @@ # Keep this list limited to the stable generic support subset extracted into :foundation-support. network/crypta/node/FSParseException* +network/crypta/node/FastRunnable* +network/crypta/node/SemiOrderedShutdownHook* network/crypta/io/WritableToDataOutputStream* network/crypta/support/api/Bucket* network/crypta/support/api/BucketFactory* @@ -11,12 +13,18 @@ network/crypta/support/api/RandomAccessBucket* network/crypta/support/api/RandomAccessBuffer* network/crypta/support/api/ResumeContext* network/crypta/support/Base64* +network/crypta/support/DoublyLinkedList* +network/crypta/support/DoublyLinkedListImpl* +network/crypta/support/ByteArrayWrapper* network/crypta/support/Fields* network/crypta/support/HTMLEncoder* network/crypta/support/HTMLEntities* network/crypta/support/HTMLNode* network/crypta/support/HexUtil* network/crypta/support/IllegalBase64Exception* +network/crypta/support/LRUMap* +network/crypta/support/LightweightException* +network/crypta/support/PromiscuousItemException* network/crypta/support/PriorityAwareExecutor* network/crypta/support/Loader* network/crypta/support/SimpleReadOnlyArrayBucket* @@ -27,6 +35,8 @@ network/crypta/support/TimeUtil* network/crypta/support/URLDecoder* network/crypta/support/URLEncodedFormatException* network/crypta/support/URLEncoder* +network/crypta/support/VirginItemException* +network/crypta/support/WrapperKeepalive* network/crypta/support/XMLCharacterClasses* network/crypta/support/io/AtomicFileMoves* network/crypta/support/io/ArrayBucket* @@ -43,6 +53,7 @@ network/crypta/support/io/DiskSpaceChecker* network/crypta/support/io/HeaderStreams* network/crypta/support/io/IOUtils* network/crypta/support/io/InetAddressComparator* +network/crypta/support/io/Fallocate* network/crypta/support/io/FilenameGenerator* network/crypta/support/io/FilenameSanitizer* network/crypta/support/io/InsufficientDiskSpaceException* @@ -58,6 +69,7 @@ network/crypta/support/io/NullRandomAccessBuffer* network/crypta/support/io/NullWriter* network/crypta/support/io/PersistentFileTracker* network/crypta/support/io/PersistentFilenameGenerator* +network/crypta/support/io/NativeThread* network/crypta/support/io/RAFInputStream* network/crypta/support/io/RandomAccessFileOutputStream* network/crypta/support/io/ReadOnlyFileSliceBucket* diff --git a/foundation-support/src/main/java/network/crypta/node/FastRunnable.java b/foundation-support/src/main/java/network/crypta/node/FastRunnable.java new file mode 100644 index 00000000000..2eceaa9fca2 --- /dev/null +++ b/foundation-support/src/main/java/network/crypta/node/FastRunnable.java @@ -0,0 +1,23 @@ +package network.crypta.node; + +/** + * Marker interface for short, non-blocking tasks that are safe to execute inline on + * latency-sensitive threads. + * + *

    Schedulers in this codebase, including the packet sender and ticker implementations in the + * runtime layer, may check whether a task implements {@code FastRunnable}. When it does, they can + * invoke {@link #run()} directly on the calling thread to minimize scheduling overhead and wake-up + * latency. Tasks that do not implement this interface are typically offloaded to an executor. + * + *

    Implementations are expected to keep {@link #run()} extremely short and to avoid blocking + * operations such as sleep, I/O, lock contention, or long computations. A slow "fast" task can + * stall a networking or ticker thread and reduce overall throughput. Callers may invoke these tasks + * on shared infrastructure threads, so implementations should not rely on thread-local state or + * thread-affinity behavior. Unchecked exceptions are handled by the calling scheduler, but + * implementations should still avoid throwing when practical. + * + *

    Memory visibility follows the usual {@link Runnable} contract. This marker does not add any + * extra happens-before guarantees beyond those established by the scheduler that invokes {@link + * #run()}. + */ +public interface FastRunnable extends Runnable {} diff --git a/src/main/java/network/crypta/node/SemiOrderedShutdownHook.java b/foundation-support/src/main/java/network/crypta/node/SemiOrderedShutdownHook.java similarity index 88% rename from src/main/java/network/crypta/node/SemiOrderedShutdownHook.java rename to foundation-support/src/main/java/network/crypta/node/SemiOrderedShutdownHook.java index e7df1a6cac6..4fe3900a850 100644 --- a/src/main/java/network/crypta/node/SemiOrderedShutdownHook.java +++ b/foundation-support/src/main/java/network/crypta/node/SemiOrderedShutdownHook.java @@ -12,14 +12,14 @@ *

    On shutdown, this hook starts all early jobs concurrently and joins each with a fixed * per-thread timeout. It then starts late jobs and joins them with the same timeout. If an * interruption occurs while joining, it is remembered and the interrupted status is restored after - * both join loops complete. This allows the remaining joins to proceed while still propagating the - * interrupt to callers. + * both join loops are complete. This allows the remaining joins to proceed while still propagating + * the interrupt to callers. * *

    Thread safety: registration methods are synchronized. Snapshots of job lists are taken at - * {@link #run()} time to avoid holding locks during joins. Callers should register jobs before + * {@link #run()} time to avoid holding locks during joins. Callers should register jobs before the * shutdown begins. * - *

    Usage: obtain the singleton via {@link #get()} and register unstarted threads with {@link + *

    Usage: get the singleton via {@link #get()} and register unstarted threads with {@link * #addEarlyJob(Thread)} or {@link #addLateJob(Thread)}. The hook calls {@link Thread#start()}. */ @SuppressWarnings({"java:S6548", "java:S2142"}) @@ -91,7 +91,7 @@ public void run() { try { r.join(TIMEOUT); } catch (InterruptedException _) { - // Remember interruption and continue joining remaining threads. + // Remember the interruption and continue joining remaining threads. wasInterrupted = true; } } @@ -108,14 +108,14 @@ public void run() { try { r.join(TIMEOUT); } catch (InterruptedException _) { - // Remember interruption and continue joining remaining threads. + // Remember the interruption and continue joining remaining threads. wasInterrupted = true; } } if (wasInterrupted) { // Restore the interrupted status after all joins so callers can observe it - // without causing subsequent joins in this method to fail immediately. + // without causing later joins in this method to fail immediately. Thread.currentThread().interrupt(); } } diff --git a/src/main/java/network/crypta/support/ByteArrayWrapper.java b/foundation-support/src/main/java/network/crypta/support/ByteArrayWrapper.java similarity index 96% rename from src/main/java/network/crypta/support/ByteArrayWrapper.java rename to foundation-support/src/main/java/network/crypta/support/ByteArrayWrapper.java index b45566a351e..51409220ff7 100644 --- a/src/main/java/network/crypta/support/ByteArrayWrapper.java +++ b/foundation-support/src/main/java/network/crypta/support/ByteArrayWrapper.java @@ -34,7 +34,7 @@ public class ByteArrayWrapper implements Comparable { /** * Comparator that first compares cached hash codes and then falls back to natural order. * - *

    This comparator may be faster for mostly-distinct values because it compares {@link + *

    This comparator may be faster for mostly distinct values because it compares {@link * #hashCode()} first (an {@code int} comparison) and only performs a full lexicographic * comparison when hash codes collide. The resulting order is total and consistent with {@link * #equals(Object)} because ties on the hash are broken by {@link #compareTo(ByteArrayWrapper)}. @@ -74,8 +74,8 @@ public boolean equals(Object other) { /** * Returns a cached, content-based hash code. * - *

    The hash is computed once from the wrapped array and stored, making subsequent calls - * constant time. The hash code is consistent with {@link #equals(Object)}. + *

    The hash is computed once from the wrapped array and stored, making later calls constant + * time. The hash code is consistent with {@link #equals(Object)}. * * @return the cached hash code based on the array contents */ diff --git a/src/main/java/network/crypta/support/DoublyLinkedList.java b/foundation-support/src/main/java/network/crypta/support/DoublyLinkedList.java similarity index 100% rename from src/main/java/network/crypta/support/DoublyLinkedList.java rename to foundation-support/src/main/java/network/crypta/support/DoublyLinkedList.java diff --git a/src/main/java/network/crypta/support/DoublyLinkedListImpl.java b/foundation-support/src/main/java/network/crypta/support/DoublyLinkedListImpl.java similarity index 99% rename from src/main/java/network/crypta/support/DoublyLinkedListImpl.java rename to foundation-support/src/main/java/network/crypta/support/DoublyLinkedListImpl.java index 4c2d0dc39ba..58b2bb30675 100644 --- a/src/main/java/network/crypta/support/DoublyLinkedListImpl.java +++ b/foundation-support/src/main/java/network/crypta/support/DoublyLinkedListImpl.java @@ -160,7 +160,7 @@ public final T shift() { * this returns an empty list and does not modify this list. * * @param n number of items to remove; non-positive values yield an empty result - * @return a list containing the removed prefix, preserving original order + * @return a list containing the removed prefix, preserving the original order */ @Override public DoublyLinkedList shift(int n) { @@ -207,7 +207,7 @@ public final T pop() { * this returns an empty list and does not modify this list. * * @param n number of items to remove; non-positive values yield an empty result - * @return a list containing the removed suffix, preserving original order + * @return a list containing the removed suffix, preserving the original order */ @Override public DoublyLinkedList pop(int n) { @@ -275,7 +275,7 @@ public final T prev(T i) { */ @Override public T remove(T i) { - if (i.getParent() == null || isEmpty()) return null; // not in list + if (i.getParent() == null || isEmpty()) return null; // not in the list if (i.getParent() != this) throw new PromiscuousItemException(i, i.getParent()); T next = i.getNext(); diff --git a/src/main/java/network/crypta/support/LRUMap.java b/foundation-support/src/main/java/network/crypta/support/LRUMap.java similarity index 96% rename from src/main/java/network/crypta/support/LRUMap.java rename to foundation-support/src/main/java/network/crypta/support/LRUMap.java index 37125081b30..44ce6b81a67 100644 --- a/src/main/java/network/crypta/support/LRUMap.java +++ b/foundation-support/src/main/java/network/crypta/support/LRUMap.java @@ -11,11 +11,11 @@ import org.slf4j.LoggerFactory; /** - * Least-recently-used (LRU) map from keys to values. + * Least-recently used (LRU) map from keys to values. * *

    When a mapping is {@linkplain #push(Object, Object) pushed}, the entry becomes the most * recently used, even if the key already exists. Removal and peeking operations work from the - * least-recently-used side (i.e., the entry that was pushed furthest in the past). The caller is + * least-recently used side (i.e., the entry that was pushed furthest in the past). The caller is * responsible for enforcing any size limits or eviction policies on top of this primitive. * *

    In many cases, a {@link java.util.LinkedHashMap} configured for access order can offer similar @@ -64,7 +64,7 @@ public LRUMap() { /** * Creates an instance reusing the provided backing map. * - *

    Implementation detail: used by safe factory methods to switch map type. + *

    Implementation detail: used by safe factory methods to switch the map type. */ private LRUMap(Map> map) { hash = map; @@ -128,7 +128,7 @@ public final synchronized V push(K key, V value) { } /** - * Removes and returns the least-recently-used key. + * Removes and returns the least-recently used key. * *

    Also removes the corresponding mapping. * @@ -150,7 +150,7 @@ public final synchronized K popKey() { } /** - * Removes and returns the least-recently-used value. + * Removes and returns the least-recently used value. * *

    Also removes the corresponding mapping. * @@ -172,7 +172,7 @@ public final synchronized V popValue() { } /** - * Returns the least-recently-used value without removing it. + * Returns the least-recently used value without removing it. * * @return the LRU value, or {@code null} if empty */ @@ -190,7 +190,7 @@ public final synchronized V peekValue() { } /** - * Returns the least-recently-used key without removing it. + * Returns the least-recently used key without removing it. * * @return the LRU key, or {@code null} if empty */ @@ -269,7 +269,7 @@ public final synchronized V get(K key) { /** * Returns a snapshot {@link Iterator} of keys in LRU→MRU order. * - *

    The snapshot is built under synchronization and is not affected by subsequent changes. + *

    The snapshot is built under synchronization and is not affected by further changes. * * @return iterator of keys from least- to most-recently-used */ @@ -287,7 +287,7 @@ public Iterator keys() { /** * Returns a snapshot {@link Iterator} of values in LRU→MRU order. * - *

    The snapshot is built under synchronization and is not affected by subsequent changes. + *

    The snapshot is built under synchronization and is not affected by further changes. * * @return iterator of values from least- to most-recently-used */ diff --git a/src/main/java/network/crypta/support/LightweightException.java b/foundation-support/src/main/java/network/crypta/support/LightweightException.java similarity index 100% rename from src/main/java/network/crypta/support/LightweightException.java rename to foundation-support/src/main/java/network/crypta/support/LightweightException.java diff --git a/src/main/java/network/crypta/support/PromiscuousItemException.java b/foundation-support/src/main/java/network/crypta/support/PromiscuousItemException.java similarity index 100% rename from src/main/java/network/crypta/support/PromiscuousItemException.java rename to foundation-support/src/main/java/network/crypta/support/PromiscuousItemException.java diff --git a/src/main/java/network/crypta/support/VirginItemException.java b/foundation-support/src/main/java/network/crypta/support/VirginItemException.java similarity index 100% rename from src/main/java/network/crypta/support/VirginItemException.java rename to foundation-support/src/main/java/network/crypta/support/VirginItemException.java diff --git a/src/main/java/network/crypta/support/WrapperKeepalive.java b/foundation-support/src/main/java/network/crypta/support/WrapperKeepalive.java similarity index 97% rename from src/main/java/network/crypta/support/WrapperKeepalive.java rename to foundation-support/src/main/java/network/crypta/support/WrapperKeepalive.java index fd48796fef5..345962db504 100644 --- a/src/main/java/network/crypta/support/WrapperKeepalive.java +++ b/foundation-support/src/main/java/network/crypta/support/WrapperKeepalive.java @@ -32,7 +32,7 @@ * *

    Threading: the instance owns its thread. {@link #close()} is thread-safe and may be invoked * from any thread. Closing requests termination; the loop exits after the current sleep interval or - * sooner if the thread is interrupted by the caller. This class never interrupts itself. + * sooner if the caller interrupts the thread. This class never interrupts itself. * *

    Interrupts: if interrupted while sleeping, the thread logs at DEBUG level, clears the * interrupted status via {@link Thread#interrupted()}, and continues looping until closed. diff --git a/src/main/java/network/crypta/support/io/Fallocate.java b/foundation-support/src/main/java/network/crypta/support/io/Fallocate.java similarity index 98% rename from src/main/java/network/crypta/support/io/Fallocate.java rename to foundation-support/src/main/java/network/crypta/support/io/Fallocate.java index f30064702bb..25040ba8032 100644 --- a/src/main/java/network/crypta/support/io/Fallocate.java +++ b/foundation-support/src/main/java/network/crypta/support/io/Fallocate.java @@ -191,7 +191,7 @@ private static int getDescriptor(FileChannel channel) { final Field field = channel.getClass().getDeclaredField("fd"); // Attempt to make the field accessible; fall back to legacy when forbidden. if (!field.canAccess(channel) && !field.trySetAccessible()) { - return 0; // Trigger legacy path when descriptor is not accessible + return 0; // Trigger a legacy path when the descriptor is not accessible } return getDescriptor((FileDescriptor) field.get(channel)); } catch (final Exception _) { @@ -212,7 +212,7 @@ private static int getDescriptor(FileDescriptor descriptor) { final Field field = descriptor.getClass().getDeclaredField(IS_ANDROID ? "descriptor" : "fd"); // Attempt to make the field accessible; fall back to legacy when forbidden. if (!field.canAccess(descriptor) && !field.trySetAccessible()) { - return 0; // Trigger legacy path when descriptor is not accessible + return 0; // Trigger a legacy path when the descriptor is not accessible } return (int) field.get(descriptor); } catch (final Exception _) { diff --git a/src/main/java/network/crypta/support/io/NativeThread.java b/foundation-support/src/main/java/network/crypta/support/io/NativeThread.java similarity index 100% rename from src/main/java/network/crypta/support/io/NativeThread.java rename to foundation-support/src/main/java/network/crypta/support/io/NativeThread.java diff --git a/src/main/java/network/crypta/node/FastRunnable.java b/src/main/java/network/crypta/node/FastRunnable.java deleted file mode 100644 index d754a7b54ae..00000000000 --- a/src/main/java/network/crypta/node/FastRunnable.java +++ /dev/null @@ -1,28 +0,0 @@ -package network.crypta.node; - -/** - * Marker interface for short, non‑blocking tasks that are safe to execute inline on - * latency‑sensitive threads. - * - *

    Schedulers in this codebase (for example {@link PacketSender} as well as ticker - * implementations such as {@link network.crypta.support.PrioritizedTicker} and {@link - * network.crypta.support.TrivialTicker}) may check whether a task implements {@code FastRunnable}. - * When it does, they can invoke {@link #run()} directly on the calling thread to minimize - * scheduling overhead and wake‑up latency. Tasks that do not implement this interface are typically - * offloaded to an executor. - * - *

    Contract for implementers: - Keep the body of {@link #run()} very short and avoid blocking - * operations (sleep, I/O, waiting on locks, long computations). A slow "fast" task can stall the - * networking/ticker thread and degrade throughput. - Assume execution occurs on a shared - * infrastructure thread; do not perform work that may rely on thread‑local context or - * thread‑affinity semantics. - Unchecked exceptions will be handled by the calling scheduler; - * implementations should avoid throwing when possible. - * - *

    Memory visibility follows the usual {@link Runnable} semantics; there are no extra - * happens‑before guarantees beyond those established by the scheduler invoking {@link #run()}. - * - * @see PacketSender - * @see network.crypta.support.PrioritizedTicker - * @see network.crypta.support.TrivialTicker - */ -public interface FastRunnable extends Runnable {} diff --git a/src/main/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSink.java b/src/main/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSink.java new file mode 100644 index 00000000000..e57bf8d0744 --- /dev/null +++ b/src/main/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSink.java @@ -0,0 +1,162 @@ +package network.crypta.node.runtime; + +import java.util.Objects; +import network.crypta.l10n.NodeL10n; +import network.crypta.node.useralerts.AbstractUserAlert; +import network.crypta.node.useralerts.UserAlert; +import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; +import network.crypta.store.alerts.StoreMaintenanceAlertKind; +import network.crypta.store.alerts.StoreMaintenanceAlertSource; +import network.crypta.support.HTMLNode; + +/** + * Bridges store-maintenance alert sources into the runtime {@link UserAlertManager}. + * + *

    The extracted store boundary exposes only live maintenance state via {@link + * StoreMaintenanceAlertSource}. This adapter keeps that boundary narrow while preserving the + * existing runtime behavior: it translates the source into a runtime-local {@link UserAlert}, + * resolves localized strings through {@link NodeL10n}, and registers the resulting alert with the + * shared alert manager used by HTTP, FCP, and other operator-facing surfaces. + * + *

    Each registration produces a lightweight wrapper that delegates back to the source whenever + * alert text or validity is queried. That means progress values stay current without the store + * having to emit pre-rendered text snapshots. The adapter is stateless apart from its reference to + * the target alert manager and is safe to reuse for multiple stores. + */ +public final class UserAlertManagerStoreAlertSink implements StoreAlertSink { + private static final String ALERTS_PREFIX = "SaltedHashCryptaStore."; + + private final UserAlertManager userAlertManager; + + /** + * Creates an adapter that forwards store-maintenance alerts into the given runtime manager. + * + *

    The manager is retained for the lifetime of this adapter and receives one runtime alert + * wrapper for each registered maintenance source. Callers typically create one sink per node + * storage subsystem and share it across multiple stores. + * + * @param userAlertManager runtime alert registry that should receive wrapped maintenance alerts; + * must not be {@code null} + * @throws NullPointerException if {@code userAlertManager} is {@code null} + */ + public UserAlertManagerStoreAlertSink(UserAlertManager userAlertManager) { + this.userAlertManager = Objects.requireNonNull(userAlertManager, "userAlertManager"); + } + + /** + * Wraps the store-maintenance source in a runtime alert and registers it with the manager. + * + *

    The wrapper remains dynamic: text, progress, and validity are computed from the source each + * time the runtime alert is queried. This preserves the old user-visible cleaner-progress + * behavior while keeping localization and HTML generation outside the store layer. + * + * @param alert live maintenance source to expose through the runtime alert system; must not be + * {@code null} + * @throws NullPointerException if {@code alert} is {@code null} + */ + @Override + public void register(StoreMaintenanceAlertSource alert) { + userAlertManager.register(new StoreMaintenanceUserAlert(alert)); + } + + private static final class StoreMaintenanceUserAlert extends AbstractUserAlert { + private static final String[] PROGRESS_PATTERNS = {"name", "processed", "total"}; + private static final String[] TITLE_PATTERNS = {"name"}; + + private final StoreMaintenanceAlertSource source; + + private StoreMaintenanceUserAlert(StoreMaintenanceAlertSource source) { + this.source = Objects.requireNonNull(source, "source"); + } + + @Override + public String anchor() { + return source.anchor(); + } + + @Override + public String dismissButtonText() { + return NodeL10n.getBase().getString("UserAlert.hide"); + } + + @Override + public HTMLNode getHTMLText() { + return new HTMLNode("#", getText()); + } + + @Override + public short getPriorityClass() { + return UserAlert.ERROR; + } + + @Override + public String getShortText() { + return localizedProgressText(shortKey()); + } + + @Override + public String getText() { + return localizedProgressText(longKey()); + } + + @Override + public String getTitle() { + return NodeL10n.getBase() + .getString(ALERTS_PREFIX + "cleanerAlertTitle", TITLE_PATTERNS, titleValues()); + } + + @Override + public boolean isValid() { + return source.isValid(); + } + + @Override + public void isValid(boolean validity) { + // Runtime validity is driven by the dynamic source. + } + + @Override + public void onDismiss() { + // Non-dismissible alert; nothing to do. + } + + @Override + public boolean shouldUnregisterOnDismiss() { + return true; + } + + @Override + public boolean userCanDismiss() { + return false; + } + + private String localizedProgressText(String key) { + return NodeL10n.getBase().getString(ALERTS_PREFIX + key, PROGRESS_PATTERNS, progressValues()); + } + + private String shortKey() { + if (source.kind() == StoreMaintenanceAlertKind.RESIZE_PROGRESS) { + return "shortResizeProgress"; + } + return source.newSlotFilter() ? "shortRebuildProgressNew" : "shortRebuildProgress"; + } + + private String longKey() { + if (source.kind() == StoreMaintenanceAlertKind.RESIZE_PROGRESS) { + return "longResizeProgress"; + } + return source.newSlotFilter() ? "longRebuildProgressNew" : "longRebuildProgress"; + } + + private String[] progressValues() { + return new String[] { + source.storeName(), String.valueOf(source.processed()), String.valueOf(source.total()) + }; + } + + private String[] titleValues() { + return new String[] {source.storeName()}; + } + } +} diff --git a/src/main/java/network/crypta/node/subsystem/NodeStorageSubsystem.java b/src/main/java/network/crypta/node/subsystem/NodeStorageSubsystem.java index 9b62c1a6514..e4b42d65db4 100644 --- a/src/main/java/network/crypta/node/subsystem/NodeStorageSubsystem.java +++ b/src/main/java/network/crypta/node/subsystem/NodeStorageSubsystem.java @@ -40,6 +40,7 @@ import network.crypta.node.NodeStoreStatsProvider; import network.crypta.node.SecurityLevels.PHYSICAL_THREAT_LEVEL; import network.crypta.node.SemiOrderedShutdownHook; +import network.crypta.node.runtime.UserAlertManagerStoreAlertSink; import network.crypta.node.stats.DataStoreInstanceType; import network.crypta.node.stats.DataStoreKeyType; import network.crypta.node.stats.DataStoreStats; @@ -58,6 +59,7 @@ import network.crypta.store.SlashdotStore; import network.crypta.store.StorableBlock; import network.crypta.store.StoreCallback; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.store.caching.CachingFreenetStore; import network.crypta.store.caching.CachingFreenetStoreTracker; import network.crypta.store.saltedhash.ResizablePersistentIntBuffer; @@ -155,7 +157,7 @@ public final class NodeStorageSubsystem { * Estimated bytes consumed per logical key across primary data and metadata structures. * *

    This heuristic drives key-count derivation from configured byte capacities. It is not a - * precise on-disk accounting number, but a stable planning estimate used for store partitioning. + * precise on-disk accounting number but a stable planning estimate used for store partitioning. */ public static final int SIZE_PER_KEY = network.crypta.keys.CHKBlock.DATA_LENGTH @@ -1864,7 +1866,7 @@ public boolean isDatabaseAwaitingPassword() { /** * Clears password-waiting flags for both client-cache and database initialization paths. * - *

    Callers typically invoke this after successfully applying credentials or resetting setup + *

    Callers typically invoke this after successfully applying credentials or resetting the setup * state. */ public void clearAwaitingPasswords() { @@ -2233,12 +2235,14 @@ public void initNoClientCacheFS() { private void finishInitSaltHashFS() { if (node.services().clientCore().getAlerts() == null) throw new NullPointerException(); - chkDatastore.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); - chkDatacache.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); - pubKeyDatastore.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); - pubKeyDatacache.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); - sskDatastore.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); - sskDatacache.getStore().setUserAlertManager(node.services().clientCore().getAlerts()); + StoreAlertSink storeAlertSink = + new UserAlertManagerStoreAlertSink(node.services().clientCore().getAlerts()); + chkDatastore.getStore().setStoreAlertSink(storeAlertSink); + chkDatacache.getStore().setStoreAlertSink(storeAlertSink); + pubKeyDatastore.getStore().setStoreAlertSink(storeAlertSink); + pubKeyDatacache.getStore().setStoreAlertSink(storeAlertSink); + sskDatastore.getStore().setStoreAlertSink(storeAlertSink); + sskDatacache.getStore().setStoreAlertSink(storeAlertSink); } /** diff --git a/src/main/java/network/crypta/store/FreenetStore.java b/src/main/java/network/crypta/store/FreenetStore.java index 840ead75cf1..a0985266a67 100644 --- a/src/main/java/network/crypta/store/FreenetStore.java +++ b/src/main/java/network/crypta/store/FreenetStore.java @@ -3,7 +3,7 @@ import java.io.Closeable; import java.io.IOException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.Ticker; /** @@ -201,13 +201,12 @@ void put(T block, byte[] data, byte[] header, boolean overwrite, boolean oldBloc void close(); /** - * Set the user alert manager used to publish non-fatal warnings and notices related to the store - * (for example, background rebuild progress or recoverable errors). + * Set the sink used to publish non-fatal store-maintenance alerts. * - * @param userAlertManager manager used to post user-facing alerts; may be ignored by some + * @param alertSink sink used to register dynamic store-maintenance alerts; may be ignored by some * implementations. */ - void setUserAlertManager(UserAlertManager userAlertManager); + void setStoreAlertSink(StoreAlertSink alertSink); /** * Return the underlying store when this instance is a wrapper. diff --git a/src/main/java/network/crypta/store/NullFreenetStore.java b/src/main/java/network/crypta/store/NullFreenetStore.java index c07971d8b76..b61ce279470 100644 --- a/src/main/java/network/crypta/store/NullFreenetStore.java +++ b/src/main/java/network/crypta/store/NullFreenetStore.java @@ -2,7 +2,7 @@ import java.io.IOException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.Ticker; /** @@ -205,9 +205,9 @@ public boolean start(Ticker ticker, boolean longStart) throws IOException { return false; } - /** Sets a {@link UserAlertManager}; ignored because the store has no alerts to raise. */ + /** Sets a store alert sink; ignored because the store has no alerts to raise. */ @Override - public void setUserAlertManager(UserAlertManager userAlertManager) { + public void setStoreAlertSink(StoreAlertSink alertSink) { // No-op } diff --git a/src/main/java/network/crypta/store/ProxyFreenetStore.java b/src/main/java/network/crypta/store/ProxyFreenetStore.java index 11e1bc62e38..81ecd71e96e 100644 --- a/src/main/java/network/crypta/store/ProxyFreenetStore.java +++ b/src/main/java/network/crypta/store/ProxyFreenetStore.java @@ -2,7 +2,7 @@ import java.io.IOException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.Ticker; /** @@ -83,10 +83,10 @@ public StoreAccessStats getTotalAccessStats() { return backDatastore.getTotalAccessStats(); } - /** {@inheritDoc} Forwards the manager to the underlying store. */ + /** {@inheritDoc} Forwards the sink to the underlying store. */ @Override - public void setUserAlertManager(UserAlertManager userAlertManager) { - this.backDatastore.setUserAlertManager(userAlertManager); + public void setStoreAlertSink(StoreAlertSink alertSink) { + this.backDatastore.setStoreAlertSink(alertSink); } /** {@inheritDoc} Returns the delegate for this proxy. */ diff --git a/src/main/java/network/crypta/store/RAMFreenetStore.java b/src/main/java/network/crypta/store/RAMFreenetStore.java index 44388e3f46e..a629fe2e89f 100644 --- a/src/main/java/network/crypta/store/RAMFreenetStore.java +++ b/src/main/java/network/crypta/store/RAMFreenetStore.java @@ -5,7 +5,7 @@ import java.util.Iterator; import network.crypta.keys.KeyVerifyException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.ByteArrayWrapper; import network.crypta.support.LRUMap; import network.crypta.support.Ticker; @@ -353,7 +353,7 @@ public boolean start(Ticker ticker, boolean longStart) throws IOException { } @Override - public void setUserAlertManager(UserAlertManager userAlertManager) { + public void setStoreAlertSink(StoreAlertSink alertSink) { // Do nothing } diff --git a/src/main/java/network/crypta/store/SlashdotStore.java b/src/main/java/network/crypta/store/SlashdotStore.java index 1d2a5a867ec..dbb436feb88 100644 --- a/src/main/java/network/crypta/store/SlashdotStore.java +++ b/src/main/java/network/crypta/store/SlashdotStore.java @@ -8,12 +8,12 @@ import java.util.List; import network.crypta.keys.KeyVerifyException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.ByteArrayWrapper; import network.crypta.support.LRUMap; import network.crypta.support.Ticker; import network.crypta.support.api.Bucket; -import network.crypta.support.io.TempBucketFactory; +import network.crypta.support.api.BucketFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,7 +29,7 @@ *

  • Strict time‑based expiration: entries older than {@code maxLifetime} are removed. * * - *

    Block payloads are written into temporary buckets provided by {@link TempBucketFactory}; + *

    Block payloads are written into temporary buckets provided by {@link BucketFactory}; * implementations commonly encrypt buckets at rest. The in‑memory index maps routing keys to disk * buckets and is synchronized for thread safety. Disk I/O is performed outside critical sections to * avoid holding locks during blocking operations. @@ -45,7 +45,7 @@ private static class DiskBlock { long lastAccessed; } - private final TempBucketFactory bf; + private final BucketFactory bf; private long maxLifetime; @@ -81,7 +81,7 @@ private static class DiskBlock { * regardless of LRU position * @param purgePeriod interval in milliseconds between scheduled purge runs * @param ticker scheduler used for periodic purge tasks - * @param tbf factory for on‑disk temporary buckets used to persist cached bytes + * @param bucketFactory factory for on‑disk temporary buckets used to persist cached bytes */ public SlashdotStore( StoreCallback callback, @@ -89,11 +89,11 @@ public SlashdotStore( long maxLifetime, long purgePeriod, Ticker ticker, - TempBucketFactory tbf) { + BucketFactory bucketFactory) { this.callback = callback; this.blocksByRoutingKey = LRUMap.createSafeMap(ByteArrayWrapper.FAST_COMPARATOR); this.maxKeys = maxKeys; - this.bf = tbf; + this.bf = bucketFactory; this.ticker = ticker; this.maxLifetime = maxLifetime; this.purgePeriod = purgePeriod; @@ -409,7 +409,7 @@ public boolean start(Ticker ticker, boolean longStart) throws IOException { /** No‑op: this store does not surface user alerts. */ @Override - public void setUserAlertManager(UserAlertManager userAlertManager) { + public void setStoreAlertSink(StoreAlertSink alertSink) { // Intentionally no operation } diff --git a/src/main/java/network/crypta/store/saltedhash/CipherManager.java b/src/main/java/network/crypta/store/saltedhash/CipherManager.java index c2521d4c596..9ba4c699050 100644 --- a/src/main/java/network/crypta/store/saltedhash/CipherManager.java +++ b/src/main/java/network/crypta/store/saltedhash/CipherManager.java @@ -11,7 +11,6 @@ import network.crypta.crypt.SHA256; import network.crypta.crypt.UnsupportedCipherException; import network.crypta.crypt.ciphers.Rijndael; -import network.crypta.node.MasterKeys; import network.crypta.support.ByteArrayWrapper; /** @@ -223,11 +222,11 @@ PCFBMode makeCipher(byte[] iv, byte[] key) { /** * Clears sensitive material held by this instance. * - *

    Overwrites the in-memory {@code salt} and {@code diskSalt} arrays via {@link - * MasterKeys#clear(byte[])}. After shutdown, this instance should not be used. + *

    Overwrites the in-memory {@code salt} and {@code diskSalt} arrays directly. After shutdown, + * this instance should not be used. */ public void shutdown() { - MasterKeys.clear(salt); - MasterKeys.clear(diskSalt); + Arrays.fill(salt, (byte) 0); + Arrays.fill(diskSalt, (byte) 0); } } diff --git a/src/main/java/network/crypta/store/saltedhash/SaltedHashFreenetStore.java b/src/main/java/network/crypta/store/saltedhash/SaltedHashFreenetStore.java index 7dc371ba8e0..e4061b5c0de 100644 --- a/src/main/java/network/crypta/store/saltedhash/SaltedHashFreenetStore.java +++ b/src/main/java/network/crypta/store/saltedhash/SaltedHashFreenetStore.java @@ -32,26 +32,24 @@ import network.crypta.crypt.ciphers.Rijndael; import network.crypta.keys.KeyVerifyException; import network.crypta.keys.SSKBlock; -import network.crypta.l10n.NodeL10n; import network.crypta.node.FastRunnable; import network.crypta.node.SemiOrderedShutdownHook; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.AbstractUserAlert; -import network.crypta.node.useralerts.UserAlert; -import network.crypta.node.useralerts.UserAlertManager; import network.crypta.store.BlockMetadata; import network.crypta.store.FetchOptions; import network.crypta.store.FreenetStore; import network.crypta.store.KeyCollisionException; import network.crypta.store.StorableBlock; import network.crypta.store.StoreCallback; +import network.crypta.store.alerts.StoreAlertSink; +import network.crypta.store.alerts.StoreMaintenanceAlertKind; +import network.crypta.store.alerts.StoreMaintenanceAlertSource; import network.crypta.support.Fields; -import network.crypta.support.HTMLNode; import network.crypta.support.HexUtil; import network.crypta.support.Ticker; import network.crypta.support.WrapperKeepalive; +import network.crypta.support.io.AtomicFileMoves; import network.crypta.support.io.Fallocate; -import network.crypta.support.io.FileUtil; import network.crypta.support.io.NativeThread; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -128,9 +126,6 @@ public final class SaltedHashFreenetStore implements Fr private static final String META_EXT = ".metadata"; private static final String CACHE_WAS = " cache was "; private static final String STR_FROM = " from "; - private static final String KEY_PROCESSED = "processed"; - private static final String KEY_TOTAL = "total"; - // Legacy debug gates removed; prefer SLF4J guards directly. private final File baseDir; @@ -277,7 +272,7 @@ private SaltedHashFreenetStore(SaltedHashStoreParams params) throws IOExcepti registerShutdown(params.shutdownHook()); cleanerThread = new Cleaner(); - cleanerStatusUserAlert = new CleanerStatusUserAlert(cleanerThread); + cleanerStatusAlertSource = new CleanerStatusAlertSource(cleanerThread); // Finish all resizing before continue if requested. maybeCompleteResizeOnStart(params.resizeOnStart()); @@ -1798,7 +1793,7 @@ private void writeConfigFile() { raf.getFD().sync(); } - FileUtil.moveTo(tempConfig, configFile); + AtomicFileMoves.moveTo(tempConfig, configFile); } catch (IOException ioe) { LOG.error("error writing config file for {}", name, ioe); } finally { @@ -1812,7 +1807,7 @@ private void writeConfigFile() { private final Condition cleanerCondition = cleanerLock.newCondition(); private static final Lock cleanerGlobalLock = new ReentrantLock(); // global across all datastore private final Cleaner cleanerThread; - private final CleanerStatusUserAlert cleanerStatusUserAlert; + private final StoreMaintenanceAlertSource cleanerStatusAlertSource; private final Entry notModified = new Entry(); @@ -2420,10 +2415,10 @@ private void writeBackIfDirty(long startFileOffset, ByteBuffer buf, boolean dirt } } - private final class CleanerStatusUserAlert extends AbstractUserAlert { + private final class CleanerStatusAlertSource implements StoreMaintenanceAlertSource { private final Cleaner cleaner; - private CleanerStatusUserAlert(Cleaner cleaner) { + private CleanerStatusAlertSource(Cleaner cleaner) { this.cleaner = cleaner; } @@ -2433,113 +2428,46 @@ public String anchor() { } @Override - public String dismissButtonText() { - return NodeL10n.getBase().getString("UserAlert.hide"); + public String storeName() { + return name; } @Override - public HTMLNode getHTMLText() { - return new HTMLNode("#", getText()); + public StoreMaintenanceAlertKind kind() { + return cleaner.isResizing + ? StoreMaintenanceAlertKind.RESIZE_PROGRESS + : StoreMaintenanceAlertKind.REBUILD_PROGRESS; } @Override - public short getPriorityClass() { - return UserAlert.ERROR; // So everyone sees it. - } - - @Override - public String getShortText() { - if (cleaner.isResizing) - return NodeL10n.getBase() - .getString( - "SaltedHashCryptaStore.shortResizeProgress", // - new String[] {"name", KEY_PROCESSED, KEY_TOTAL}, // - new String[] { - name, - String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft), - String.valueOf(cleaner.entriesTotal) - }); - else - return NodeL10n.getBase() - .getString( - "SaltedHashCryptaStore.shortRebuildProgress" - + ((slotFilter != null && slotFilter.isNew()) ? "New" : ""), - new String[] {"name", KEY_PROCESSED, KEY_TOTAL}, // - new String[] { - name, - String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft), - String.valueOf(cleaner.entriesTotal) - }); + public long processed() { + return cleaner.entriesTotal - cleaner.entriesLeft; } @Override - public String getText() { - if (cleaner.isResizing) - return NodeL10n.getBase() - .getString( - "SaltedHashCryptaStore.longResizeProgress", // - new String[] {"name", KEY_PROCESSED, KEY_TOTAL}, // - new String[] { - name, - String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft), - String.valueOf(cleaner.entriesTotal) - }); - else - return NodeL10n.getBase() - .getString( - "SaltedHashCryptaStore.longRebuildProgress" - + ((slotFilter != null && slotFilter.isNew()) ? "New" : ""), - new String[] {"name", KEY_PROCESSED, KEY_TOTAL}, - new String[] { - name, - String.valueOf(cleaner.entriesTotal - cleaner.entriesLeft), - String.valueOf(cleaner.entriesTotal) - }); + public long total() { + return cleaner.entriesTotal; } @Override - public String getTitle() { - return NodeL10n.getBase() - .getString( - "SaltedHashCryptaStore.cleanerAlertTitle", // - new String[] {"name"}, // - new String[] {name}); + public boolean newSlotFilter() { + return slotFilter != null && slotFilter.isNew(); } @Override public boolean isValid() { return cleaner.isRebuilding || cleaner.isResizing; } - - @Override - public void isValid(boolean validity) { - // Ignore - } - - @Override - public void onDismiss() { - // Ignore - } - - @Override - public boolean shouldUnregisterOnDismiss() { - return true; - } - - @Override - public boolean userCanDismiss() { - return false; - } } /** - * Registers the cleaner status alert with the provided manager so progress appears in the UI. + * Registers the cleaner status alert source with the provided sink so progress appears in the UI. * - * @param userAlertManager destination manager; must not be {@code null} + * @param alertSink destination sink; when {@code null}, {@link StoreAlertSink#NO_OP} is used */ @Override - public void setUserAlertManager(UserAlertManager userAlertManager) { - if (cleanerStatusUserAlert != null) userAlertManager.register(cleanerStatusUserAlert); + public void setStoreAlertSink(StoreAlertSink alertSink) { + Objects.requireNonNullElse(alertSink, StoreAlertSink.NO_OP).register(cleanerStatusAlertSource); } /** diff --git a/src/test/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSinkTest.java b/src/test/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSinkTest.java new file mode 100644 index 00000000000..3f13660107a --- /dev/null +++ b/src/test/java/network/crypta/node/runtime/UserAlertManagerStoreAlertSinkTest.java @@ -0,0 +1,237 @@ +package network.crypta.node.runtime; + +import java.lang.reflect.Method; +import network.crypta.l10n.BaseL10n.LANGUAGE; +import network.crypta.l10n.BaseL10n; +import network.crypta.l10n.L10nTestUtils; +import network.crypta.l10n.NodeL10n; +import network.crypta.node.useralerts.UserAlert; +import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreMaintenanceAlertKind; +import network.crypta.store.alerts.StoreMaintenanceAlertSource; +import network.crypta.support.HTMLNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("java:S100") +class UserAlertManagerStoreAlertSinkTest { + + @Mock private UserAlertManager userAlertManager; + + private BaseL10n originalBase; + + @BeforeEach + void setUp() throws ReflectiveOperationException { + originalBase = NodeL10n.getBase(); + installBase(L10nTestUtils.createL10n(LANGUAGE.ENGLISH)); + } + + @AfterEach + void tearDown() throws ReflectiveOperationException { + installBase(originalBase); + } + + @Test + void constructor_whenUserAlertManagerNull_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new UserAlertManagerStoreAlertSink(null)); + } + + @Test + void register_whenResizeAlert_registersLocalizedNonDismissableErrorAlert() { + UserAlertManagerStoreAlertSink sink = new UserAlertManagerStoreAlertSink(userAlertManager); + MutableAlertSource source = + new MutableAlertSource( + "store-cleaner-chk", + "CHK", + StoreMaintenanceAlertKind.RESIZE_PROGRESS, + 7, + 12, + false, + true); + + sink.register(source); + + UserAlert alert = captureRegisteredAlert(); + + assertEquals(source.anchor(), alert.anchor()); + assertEquals(localizedTitle(source), alert.getTitle()); + assertEquals(localizedProgress("shortResizeProgress", source), alert.getShortText()); + assertEquals(localizedProgress("longResizeProgress", source), alert.getText()); + assertEquals(UserAlert.ERROR, alert.getPriorityClass()); + assertFalse(alert.userCanDismiss()); + assertTrue(alert.shouldUnregisterOnDismiss()); + assertEquals(NodeL10n.getBase().getString("UserAlert.hide"), alert.dismissButtonText()); + assertTrue(alert.isValid()); + assertFalse(alert.isEventNotification()); + + HTMLNode html = alert.getHTMLText(); + assertNotNull(html); + assertEquals("#", html.getName()); + assertEquals(alert.getText(), html.getContent()); + } + + @Test + void register_whenRebuildAlertWithNewSlotFilter_usesNewFormatTextAndTracksDynamicValidity() { + UserAlertManagerStoreAlertSink sink = new UserAlertManagerStoreAlertSink(userAlertManager); + MutableAlertSource source = + new MutableAlertSource( + "store-cleaner-ssk", + "SSK", + StoreMaintenanceAlertKind.REBUILD_PROGRESS, + 3, + 9, + true, + true); + + sink.register(source); + + UserAlert alert = captureRegisteredAlert(); + + assertEquals(localizedProgress("shortRebuildProgressNew", source), alert.getShortText()); + assertEquals(localizedProgress("longRebuildProgressNew", source), alert.getText()); + + alert.isValid(false); + assertTrue(alert.isValid()); + + source.valid = false; + assertFalse(alert.isValid()); + assertDoesNotThrow(alert::onDismiss); + } + + @Test + void register_whenRebuildAlertWithoutNewSlotFilter_usesLegacyRebuildText() { + UserAlertManagerStoreAlertSink sink = new UserAlertManagerStoreAlertSink(userAlertManager); + MutableAlertSource source = + new MutableAlertSource( + "store-cleaner-pubkey", + "PubKey", + StoreMaintenanceAlertKind.REBUILD_PROGRESS, + 11, + 40, + false, + true); + + sink.register(source); + + UserAlert alert = captureRegisteredAlert(); + + assertEquals(localizedProgress("shortRebuildProgress", source), alert.getShortText()); + assertEquals(localizedProgress("longRebuildProgress", source), alert.getText()); + } + + @Test + void register_whenSourceNull_throwsNullPointerException() { + UserAlertManagerStoreAlertSink sink = new UserAlertManagerStoreAlertSink(userAlertManager); + + assertThrows(NullPointerException.class, () -> sink.register(null)); + + verifyNoInteractions(userAlertManager); + } + + private UserAlert captureRegisteredAlert() { + ArgumentCaptor captor = ArgumentCaptor.forClass(UserAlert.class); + verify(userAlertManager).register(captor.capture()); + return captor.getValue(); + } + + private static String localizedTitle(StoreMaintenanceAlertSource source) { + return NodeL10n.getBase() + .getString( + "SaltedHashCryptaStore.cleanerAlertTitle", + new String[] {"name"}, + new String[] {source.storeName()}); + } + + private static String localizedProgress(String key, StoreMaintenanceAlertSource source) { + return NodeL10n.getBase() + .getString( + "SaltedHashCryptaStore." + key, + new String[] {"name", "processed", "total"}, + new String[] { + source.storeName(), String.valueOf(source.processed()), String.valueOf(source.total()) + }); + } + + private static void installBase(BaseL10n base) throws ReflectiveOperationException { + Method setBase = NodeL10n.class.getDeclaredMethod("setBase", BaseL10n.class); + setBase.setAccessible(true); + setBase.invoke(null, base); + } + + private static final class MutableAlertSource implements StoreMaintenanceAlertSource { + private final String anchor; + private final String storeName; + private final StoreMaintenanceAlertKind kind; + private final long processed; + private final long total; + private final boolean newSlotFilter; + private boolean valid; + + private MutableAlertSource( + String anchor, + String storeName, + StoreMaintenanceAlertKind kind, + long processed, + long total, + boolean newSlotFilter, + boolean valid) { + this.anchor = anchor; + this.storeName = storeName; + this.kind = kind; + this.processed = processed; + this.total = total; + this.newSlotFilter = newSlotFilter; + this.valid = valid; + } + + @Override + public String anchor() { + return anchor; + } + + @Override + public String storeName() { + return storeName; + } + + @Override + public StoreMaintenanceAlertKind kind() { + return kind; + } + + @Override + public long processed() { + return processed; + } + + @Override + public long total() { + return total; + } + + @Override + public boolean newSlotFilter() { + return newSlotFilter; + } + + @Override + public boolean isValid() { + return valid; + } + } +} diff --git a/src/test/java/network/crypta/store/NullFreenetStoreTest.java b/src/test/java/network/crypta/store/NullFreenetStoreTest.java index 67af5be83ae..db29d6d4f2d 100644 --- a/src/test/java/network/crypta/store/NullFreenetStoreTest.java +++ b/src/test/java/network/crypta/store/NullFreenetStoreTest.java @@ -3,7 +3,7 @@ import java.io.IOException; import network.crypta.crypt.DSAPublicKey; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.Ticker; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -198,13 +198,13 @@ void start_whenCalled_returnsFalse() throws IOException { } @Test - void setUserAlertManager_whenCalled_noInteractions() { + void setStoreAlertSink_whenCalled_noInteractions() { try (NullFreenetStore store = new NullFreenetStore<>(new TestCallback())) { - UserAlertManager manager = org.mockito.Mockito.mock(UserAlertManager.class); + StoreAlertSink alertSink = org.mockito.Mockito.mock(StoreAlertSink.class); - store.setUserAlertManager(manager); + store.setStoreAlertSink(alertSink); - verifyNoInteractions(manager); + verifyNoInteractions(alertSink); } } diff --git a/src/test/java/network/crypta/store/RAMFreenetStoreTest.java b/src/test/java/network/crypta/store/RAMFreenetStoreTest.java index 188296e1d44..564696cd691 100644 --- a/src/test/java/network/crypta/store/RAMFreenetStoreTest.java +++ b/src/test/java/network/crypta/store/RAMFreenetStoreTest.java @@ -4,7 +4,7 @@ import java.util.Arrays; import network.crypta.keys.KeyVerifyException; import network.crypta.node.stats.StoreAccessStats; -import network.crypta.node.useralerts.UserAlertManager; +import network.crypta.store.alerts.StoreAlertSink; import network.crypta.support.Ticker; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -444,9 +444,9 @@ void misc_accessorsAndNoops_behaveAsDocumented() throws Exception { assertFalse(store.start(mock(Ticker.class), true)); assertNull(store.getTotalAccessStats()); - UserAlertManager mgr = mock(UserAlertManager.class); - store.setUserAlertManager(mgr); - verifyNoInteractions(mgr); + StoreAlertSink alertSink = mock(StoreAlertSink.class); + store.setStoreAlertSink(alertSink); + verifyNoInteractions(alertSink); // Clear removes everything store.put(mockBlock(bytes(1), bytes(1)), bytes(1), bytes(0), false, false); @@ -531,7 +531,7 @@ void migrateTo_whenConstructThrows_skipsThatEntry() throws Exception { when(sourceCb.storeFullKeys()).thenReturn(false); when(sourceCb.collisionPossible()).thenReturn(true); - // First construct throws, second succeeds + // The first construct throws, the second succeeds when(sourceCb.construct( any(StoreCallback.BlockPayload.class), any(StoreCallback.ConstructOptions.class), diff --git a/src/test/java/network/crypta/store/SlashdotStoreTest.java b/src/test/java/network/crypta/store/SlashdotStoreTest.java index c10eba03165..fb1d3aefa7c 100644 --- a/src/test/java/network/crypta/store/SlashdotStoreTest.java +++ b/src/test/java/network/crypta/store/SlashdotStoreTest.java @@ -18,7 +18,9 @@ import network.crypta.support.SpeedyTicker; import network.crypta.support.TrivialTicker; import network.crypta.support.api.Bucket; +import network.crypta.support.api.BucketFactory; import network.crypta.support.compress.Compressor; +import network.crypta.support.io.ArrayBucket; import network.crypta.support.io.ArrayBucketFactory; import network.crypta.support.io.BucketTools; import network.crypta.support.io.FileUtil; @@ -30,6 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -38,7 +41,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SuppressWarnings("java:S100") @@ -313,6 +318,29 @@ void start_returnsFalse() throws IOException { } } + @Test + void putAndFetch_whenUsingGenericBucketFactory_roundTripsThroughBucketFactory() throws Exception { + BucketFactory bucketFactory = mock(BucketFactory.class); + when(bucketFactory.makeBucket(anyLong())).thenAnswer(invocation -> new ArrayBucket()); + + TestCallback cb = new TestCallback(); + try (SlashdotStore ss = + new SlashdotStore<>(cb, 10, 60_000, 1_000, new SpeedyTicker(), bucketFactory)) { + byte[] routingKey = new byte[] {1, 2, 3}; + byte[] fullKey = new byte[] {4, 5, 6, 7}; + byte[] header = new byte[] {8, 9}; + byte[] data = new byte[] {10, 11, 12}; + + ss.put(new TestBlock(routingKey, fullKey), data, header, false, false); + TestBlock fetched = ss.fetch(routingKey, fullKey, false, false, true, false, null); + + assertNotNull(fetched); + assertArrayEquals(routingKey, fetched.getRoutingKey()); + assertArrayEquals(fullKey, fetched.getFullKey()); + verify(bucketFactory).makeBucket(9L); + } + } + // --- helpers --- private static final class TestBlock implements StorableBlock {