platform-java 2.0 introduces persistent storage volumes, allowing applications to store data that survives restarts and redeployments. Each application can mount one or more volumes for database files, caches, logs, or any persistent data.
-
Persistent Volumes:
- Data survives application stop/restart
- Data survives application undeploy (configurable)
- Suitable for databases, user files, configuration
-
Ephemeral Volumes:
- Data survives application stop/restart within same deployment
- Data deleted on undeploy
- Suitable for caches, temporary files, session data
Each volume has:
- Name: Unique identifier within the application (e.g., "database", "cache")
- Mount Path: Logical path for application code (e.g., "/var/myapp/db")
- Persistent Flag: true = data survives undeploy, false = ephemeral
- Size Limit: Optional maximum size in MB (0 = unlimited)
Volumes are stored on the filesystem at:
/var/platform-java/volumes/{applicationId}/{volumeName}/
Example:
/var/platform-java/volumes/
my-app/
database/ ← Persistent database files
cache/ ← Ephemeral cache files
logs/ ← Persistent log files
The base path can be customized via system property:
java -Dplatform-java.volumes.dir=/custom/path -jar platform-java-launcher.jarStore SQLite database files that survive restarts:
ApplicationDescriptor descriptor = ApplicationDescriptor.builder()
.applicationId("database-app")
.mainClass("com.example.DatabaseApp")
.addVolume(new VolumeMount(
"database", // Volume name
"/var/myapp/db", // Mount path (logical)
true, // Persistent = true
1024 // 1GB size limit
))
.build();Application usage:
public class DatabaseApp implements Application {
@Override
public void start(ApplicationContext context) {
Path dbPath = context.getVolumeManager()
.map(vm -> vm.getVolumePath("database"))
.orElseThrow();
// Use dbPath for file I/O
Path dbFile = dbPath.resolve("myapp.db");
// Initialize SQLite, H2, etc.
}
}Application with both persistent and ephemeral volumes:
ApplicationDescriptor descriptor = ApplicationDescriptor.builder()
.applicationId("multi-volume-app")
.mainClass("com.example.MultiVolumeApp")
// Persistent database (survives undeploy)
.addVolume(new VolumeMount("database", "/data/db", true, 2048))
// Ephemeral cache (deleted on undeploy)
.addVolume(new VolumeMount("cache", "/data/cache", false, 512))
// Persistent logs (no size limit)
.addVolume(new VolumeMount("logs", "/data/logs", true, 0))
.build();Application usage:
public class MultiVolumeApp implements Application {
@Override
public void start(ApplicationContext context) {
VolumeManager vm = context.getVolumeManager().orElseThrow();
Path dbPath = vm.getVolumePath("database");
Path cachePath = vm.getVolumePath("cache");
Path logsPath = vm.getVolumePath("logs");
// Initialize services with these paths
initDatabase(dbPath);
initCache(cachePath);
initLogging(logsPath);
}
}Ephemeral volume for session data:
ApplicationDescriptor descriptor = ApplicationDescriptor.builder()
.applicationId("web-app")
.mainClass("com.example.WebApp")
.addVolume(new VolumeMount(
"sessions", // Volume name
"/var/sessions", // Mount path
false, // Ephemeral = false (deleted on undeploy)
256 // 256MB limit
))
.build();Applications without volumes work as before:
ApplicationDescriptor descriptor = ApplicationDescriptor.builder()
.applicationId("stateless-app")
.mainClass("com.example.StatelessApp")
// No volumes defined
.build();
// context.getVolumeManager() returns Optional.empty()Volumes can be configured in YAML descriptors:
applicationId: my-app
mainClass: com.example.MyApp
volumes:
- name: database
mountPath: /var/myapp/db
persistent: true
maxSizeMB: 1024
- name: cache
mountPath: /var/myapp/cache
persistent: false
maxSizeMB: 512
- name: logs
mountPath: /var/myapp/logs
persistent: true
maxSizeMB: 0 # No limitpublic class MyApp implements Application {
@Override
public void start(ApplicationContext context) throws Exception {
// Get VolumeManager (Optional)
Optional<VolumeManager> vmOpt = context.getVolumeManager();
if (vmOpt.isPresent()) {
VolumeManager vm = vmOpt.get();
// Get volume path
Path dbPath = vm.getVolumePath("database");
// Check if volume exists and is accessible
if (vm.volumeExists("database")) {
// Use the volume
Files.createFile(dbPath.resolve("data.db"));
}
// List all volumes
List<VolumeMount> volumes = vm.getVolumes();
for (VolumeMount vol : volumes) {
System.out.println("Volume: " + vol.getName() +
" at " + vm.getVolumePath(vol.getName()));
}
// Check volume usage
long usageBytes = vm.getVolumeUsageBytes("database");
long limitBytes = vm.getVolumeSizeLimit("database");
System.out.println("Database using " + usageBytes + " of " + limitBytes + " bytes");
// Check if persistent
boolean isPersistent = vm.isPersistent("database");
} else {
// No volumes defined for this application
System.out.println("Application has no volumes");
}
}
}VolumeManager vm = context.getVolumeManager().orElseThrow();
// Get absolute path to volume directory
Path volumePath = vm.getVolumePath("database");
// Write files
Path dataFile = volumePath.resolve("data.json");
Files.writeString(dataFile, "{\"key\": \"value\"}");
// Read files
String content = Files.readString(dataFile);
// Create subdirectories
Path subdir = volumePath.resolve("subdirectory");
Files.createDirectory(subdir);
// Check volume size
long usage = vm.getVolumeUsageBytes("database");
long limit = vm.getVolumeSizeLimit("database");
if (limit > 0 && usage > limit) {
throw new IOException("Volume quota exceeded");
}
// List all volumes
for (VolumeMount vol : vm.getVolumes()) {
System.out.println(vol.getName() + " persistent=" + vol.isPersistent());
}When an application is deployed:
- Platform reads volume definitions from ApplicationDescriptor
- Creates FileSystemVolumeManager for the application
- Creates volume directories at
/var/platform-java/volumes/{appId}/{volumeName} - Adds VolumeManager to ApplicationContext
// During ApplicationManager.deploy()
VolumeManager volumeManager = new FileSystemVolumeManager(
applicationId,
descriptor.getVolumes()
);
ApplicationContext context = ApplicationContextImpl.builder()
.volumeManager(volumeManager)
.build();Volumes persist across start/stop operations:
deploy() → volumes created
start() → application accesses volumes
stop() → volumes remain intact
start() → application accesses same data
When an application is undeployed:
- Ephemeral volumes: Automatically deleted
- Persistent volumes: Kept on disk (by default)
// During ApplicationManager.undeploy()
volumeManager.cleanupEphemeralVolumes();
// Persistent volumes remain at /var/platform-java/volumes/{appId}/When redeploying an application with persistent volumes:
- New deployment creates VolumeManager
- Volume directories already exist (from previous deployment)
- Application accesses existing data
undeploy() → ephemeral deleted, persistent kept
deploy() → volume directories reused
start() → application sees previous data
Volumes can have optional size limits:
new VolumeMount("database", "/data/db", true, 1024) // 1GB limitCheck usage against limit:
VolumeManager vm = context.getVolumeManager().orElseThrow();
long usage = vm.getVolumeUsageBytes("database");
long limit = vm.getVolumeSizeLimit("database");
if (limit > 0 && usage > limit * 0.9) {
logger.warn("Volume database at 90% capacity: {} / {} bytes", usage, limit);
}Automatic Cleanup (ephemeral volumes):
- Deleted automatically on undeploy
- No manual intervention required
Manual Cleanup (persistent volumes):
// In FileSystemVolumeManager
public void cleanupAllVolumes() throws IOException {
// Deletes ALL volumes (including persistent)
// Use with caution!
}Platform administrators can manually delete persistent volumes:
# List volumes
ls /var/platform-java/volumes/
# Delete specific application's volumes
rm -rf /var/platform-java/volumes/my-app/
# Delete specific volume
rm -rf /var/platform-java/volumes/my-app/old-cache/Track volume usage over time:
VolumeManager vm = context.getVolumeManager().orElseThrow();
// Periodic monitoring
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
for (VolumeMount vol : vm.getVolumes()) {
try {
long usage = vm.getVolumeUsageBytes(vol.getName());
long limit = vm.getVolumeSizeLimit(vol.getName());
double percentUsed = limit > 0 ? (usage * 100.0 / limit) : 0;
logger.info("Volume {}: {} bytes ({:.1f}%)",
vol.getName(), usage, percentUsed);
if (limit > 0 && usage > limit) {
logger.error("Volume {} exceeded size limit!", vol.getName());
}
} catch (IOException e) {
logger.error("Failed to check volume usage", e);
}
}
}, 0, 60, TimeUnit.SECONDS); // Every 60 seconds- Persistent: Databases, user uploads, configuration files, audit logs
- Ephemeral: Caches, temporary files, session data, build artifacts
// Database: set realistic limit based on expected growth
.addVolume(new VolumeMount("database", "/data/db", true, 10240)) // 10GB
// Cache: limit to prevent unbounded growth
.addVolume(new VolumeMount("cache", "/data/cache", false, 512)) // 512MB
// Logs: no limit, but implement log rotation
.addVolume(new VolumeMount("logs", "/data/logs", true, 0)) // UnlimitedTrack usage and alert when approaching limits:
long usage = vm.getVolumeUsageBytes("database");
long limit = vm.getVolumeSizeLimit("database");
if (limit > 0 && usage > limit * 0.8) {
alerting.sendWarning("Database volume at 80% capacity");
}try {
Path dbPath = vm.getVolumePath("database");
Files.writeString(dbPath.resolve("data.txt"), content);
} catch (NoSuchFileException e) {
logger.error("Volume directory does not exist", e);
} catch (FileSystemException e) {
logger.error("Disk full or permission denied", e);
} catch (IOException e) {
logger.error("Failed to write to volume", e);
}For persistent volumes, implement data retention policies:
public void cleanupOldLogs(VolumeManager vm) throws IOException {
Path logsPath = vm.getVolumePath("logs");
long cutoffTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30);
Files.walk(logsPath)
.filter(Files::isRegularFile)
.filter(path -> {
try {
return Files.getLastModifiedTime(path).toMillis() < cutoffTime;
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
logger.warn("Failed to delete old log: {}", path, e);
}
});
}Implement backup strategies for critical data:
#!/bin/bash
# Backup script for persistent volumes
BACKUP_DIR=/backups/platform-java
DATE=$(date +%Y%m%d)
# Backup all persistent volumes
for app in /var/platform-java/volumes/*; do
app_id=$(basename $app)
tar czf $BACKUP_DIR/${app_id}-${DATE}.tar.gz $app
done
# Retain only last 7 days
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete-
VolumeMount (in
platform-java-api):- Value object describing volume configuration
- Immutable with validation
-
VolumeManager (interface in
platform-java-api):- Defines volume management operations
- Accessed via
ApplicationContext.getVolumeManager()
-
FileSystemVolumeManager (in
platform-java-storage):- Filesystem-based VolumeManager implementation
- Uses Java NIO for directory operations
- Thread-safe using ConcurrentHashMap
-
ApplicationDescriptor (in
platform-java-api):- Extended with volumes list
- Builder pattern with
addVolume()method
-
ApplicationContext (in
platform-java-api):- Extended with
getVolumeManager()method - Returns
Optional<VolumeManager>
- Extended with
-
ApplicationManager (in
platform-java-core):- Creates FileSystemVolumeManager during deploy
- Cleans up ephemeral volumes on undeploy
ApplicationDescriptor.getVolumes()
↓
ApplicationManager.deploy()
↓
new FileSystemVolumeManager(appId, volumes)
↓
Creates directories at /var/platform-java/volumes/{appId}/{volumeName}
↓
ApplicationContextImpl.Builder.volumeManager(vm)
↓
ApplicationContext.getVolumeManager() → Optional<VolumeManager>
↓
Application code: vm.getVolumePath("database")
↓
File I/O operations
package org.flossware.platform-java.api;
public class VolumeMount {
public VolumeMount(String name, String mountPath, boolean persistent, long maxSizeMB);
public String getName();
public String getMountPath();
public boolean isPersistent();
public long getMaxSizeMB();
public boolean hasSizeLimit(); // true if maxSizeMB > 0
}package org.flossware.platform-java.api;
public interface VolumeManager {
Path getVolumePath(String volumeName);
List<VolumeMount> getVolumes();
long getVolumeUsageBytes(String volumeName) throws IOException;
boolean volumeExists(String volumeName);
long getVolumeSizeLimit(String volumeName); // In bytes
boolean isPersistent(String volumeName);
}package org.flossware.platform-java.storage;
public class FileSystemVolumeManager implements VolumeManager {
public FileSystemVolumeManager(String applicationId, List<VolumeMount> volumes) throws IOException;
public FileSystemVolumeManager(String applicationId, List<VolumeMount> volumes, Path basePath) throws IOException;
// VolumeManager methods...
// Management methods
public void cleanupEphemeralVolumes() throws IOException;
public void cleanupAllVolumes() throws IOException;
// Getters
public String getApplicationId();
public Path getBasePath();
}package org.flossware.platform-java.api;
public interface ApplicationContext {
// Existing methods...
/**
* Returns the volume manager if volumes are defined for this application.
* @return optional volume manager, empty if no volumes are defined
* @since 2.0
*/
Optional<VolumeManager> getVolumeManager();
}Applications without volumes continue to work without changes:
// platform-java 1.0 application (no volumes)
public class MyApp implements Application {
@Override
public void start(ApplicationContext context) {
// context.getVolumeManager() returns Optional.empty()
}
}Add volumes incrementally:
// platform-java 2.0 application (with volumes)
ApplicationDescriptor descriptor = ApplicationDescriptor.builder()
.applicationId("my-app")
.mainClass("com.example.MyApp")
.addVolume(new VolumeMount("data", "/var/data", true, 1024)) // NEW
.build();
public class MyApp implements Application {
@Override
public void start(ApplicationContext context) {
context.getVolumeManager().ifPresent(vm -> {
Path dataPath = vm.getVolumePath("data");
// Use volume
});
}
}Cause: Volume name mismatch or volumes not initialized.
Solution:
- Check volume name matches descriptor:
vm.getVolumePath("database") - Verify
getVolumeManager()returnsOptionalwith value - Check logs for volume creation errors
Cause: Insufficient filesystem permissions.
Solution:
- Ensure platform has write access to
/var/platform-java/volumes/ - Check directory ownership:
chown -R platform-java:platform-java /var/platform-java/volumes - Check SELinux/AppArmor policies
Cause: Application wrote more data than maxSizeMB limit.
Solution:
- Increase size limit in descriptor
- Implement data cleanup in application
- Monitor usage and alert before hitting limit
Cause: Volume was marked ephemeral instead of persistent.
Solution:
- Verify
persistent=truein VolumeMount constructor - Check volume definition in YAML:
persistent: true - Review logs for "Deleted ephemeral volume" messages
- Resource Enforcement - Automatic enforcement of resource limits
- Application Lifecycle - Application states and transitions
- Configuration Guide - YAML/JSON descriptor format