|
25 | 25 | import com.arcadedb.server.event.ServerEventLog; |
26 | 26 | import com.arcadedb.server.ha.HAServer; |
27 | 27 |
|
28 | | -import java.io.File; |
| 28 | +import java.lang.reflect.Constructor; |
29 | 29 | import java.lang.reflect.InvocationTargetException; |
| 30 | +import java.lang.reflect.Method; |
30 | 31 | import java.time.LocalDateTime; |
31 | 32 | import java.time.LocalTime; |
32 | 33 | import java.time.format.DateTimeFormatter; |
|
40 | 41 | public class BackupTask implements Runnable { |
41 | 42 | private static final DateTimeFormatter BACKUP_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); |
42 | 43 |
|
| 44 | + // Cached reflection objects for better performance |
| 45 | + private static volatile Class<?> backupClass; |
| 46 | + private static volatile Constructor<?> backupConstructor; |
| 47 | + private static volatile Method setDirectoryMethod; |
| 48 | + private static volatile Method setVerboseLevelMethod; |
| 49 | + private static volatile Method backupDatabaseMethod; |
| 50 | + private static volatile boolean reflectionInitialized; |
| 51 | + private static final Object REFLECTION_LOCK = new Object(); |
| 52 | + |
43 | 53 | private final ArcadeDBServer server; |
44 | 54 | private final String databaseName; |
45 | 55 | private final DatabaseBackupConfig config; |
@@ -150,48 +160,79 @@ private boolean isWithinTimeWindow() { |
150 | 160 |
|
151 | 161 | /** |
152 | 162 | * Performs the actual backup using the integration Backup class. |
| 163 | + * <p> |
| 164 | + * Note: The backup mechanism in ArcadeDB reads from immutable pages and handles |
| 165 | + * consistency internally. The transaction check is a safety warning but does not |
| 166 | + * block new transactions - the backup is designed to be non-blocking. |
153 | 167 | */ |
154 | 168 | private String performBackup() throws Exception { |
155 | 169 | final Database database = server.getDatabase(databaseName); |
156 | 170 |
|
157 | | - // Check for active transaction - never automatically rollback as it could cause data loss |
158 | | - if (database.isTransactionActive() && ((DatabaseInternal) database).getTransaction().hasChanges()) { |
159 | | - throw new BackupException("Cannot perform backup for database '" + databaseName + |
160 | | - "': active transaction with pending changes detected. Please commit or rollback the transaction manually."); |
| 171 | + // Check for active transaction - warn but don't block |
| 172 | + // ArcadeDB backup is designed to work on immutable pages, so this is informational |
| 173 | + if (database.isTransactionActive()) { |
| 174 | + final DatabaseInternal dbInternal = (DatabaseInternal) database; |
| 175 | + if (dbInternal.getTransaction().hasChanges()) { |
| 176 | + LogManager.instance().log(this, Level.WARNING, |
| 177 | + "Backup for database '%s' starting with active transaction - uncommitted changes will not be included", |
| 178 | + databaseName); |
| 179 | + } |
161 | 180 | } |
162 | 181 |
|
163 | 182 | // Generate backup filename |
164 | 183 | final String timestamp = LocalDateTime.now().format(BACKUP_TIMESTAMP_FORMAT); |
165 | 184 | final String backupFileName = databaseName + "-backup-" + timestamp + ".zip"; |
166 | 185 |
|
167 | | - // Prepare backup directory for this database |
168 | | - final String dbBackupDir = java.nio.file.Paths.get(backupDirectory, databaseName).toString(); |
169 | | - final File backupDirFile = new File(dbBackupDir); |
170 | | - if (!backupDirFile.exists()) { |
171 | | - if (!backupDirFile.mkdirs()) { |
172 | | - throw new BackupException("Failed to create backup directory for database '" + databaseName + "': " + dbBackupDir); |
173 | | - } |
| 186 | + // Prepare backup directory for this database - use Files.createDirectories to avoid TOCTOU |
| 187 | + final java.nio.file.Path dbBackupPath = java.nio.file.Paths.get(backupDirectory, databaseName); |
| 188 | + try { |
| 189 | + java.nio.file.Files.createDirectories(dbBackupPath); |
| 190 | + } catch (final java.io.IOException e) { |
| 191 | + throw new BackupException("Failed to create backup directory for database '" + databaseName + "': " + dbBackupPath, e); |
174 | 192 | } |
| 193 | + final String dbBackupDir = dbBackupPath.toString(); |
175 | 194 |
|
| 195 | + // Perform backup using cached reflection for better performance |
176 | 196 | try { |
177 | | - final Class<?> clazz = Class.forName("com.arcadedb.integration.backup.Backup"); |
178 | | - final Object backup = clazz.getConstructor(Database.class, String.class) |
179 | | - .newInstance(database, backupFileName); |
| 197 | + initializeReflection(); |
180 | 198 |
|
181 | | - clazz.getMethod("setDirectory", String.class).invoke(backup, dbBackupDir); |
182 | | - clazz.getMethod("setVerboseLevel", Integer.TYPE).invoke(backup, 1); |
| 199 | + final Object backup = backupConstructor.newInstance(database, backupFileName); |
| 200 | + setDirectoryMethod.invoke(backup, dbBackupDir); |
| 201 | + setVerboseLevelMethod.invoke(backup, 1); |
183 | 202 |
|
184 | | - final String backupFile = (String) clazz.getMethod("backupDatabase").invoke(backup); |
185 | | - return backupFile; |
| 203 | + return (String) backupDatabaseMethod.invoke(backup); |
186 | 204 |
|
187 | | - } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException e) { |
| 205 | + } catch (final IllegalAccessException | InstantiationException e) { |
188 | 206 | throw new BackupException("Backup libs not found in classpath. Make sure arcadedb-integration module is " + |
189 | 207 | "included.", e); |
190 | 208 | } catch (final InvocationTargetException e) { |
191 | 209 | throw new BackupException("Error performing backup for database '" + databaseName + "'", e.getTargetException()); |
192 | 210 | } |
193 | 211 | } |
194 | 212 |
|
| 213 | + /** |
| 214 | + * Initializes the cached reflection objects for the Backup class. |
| 215 | + * Uses double-checked locking for thread-safe lazy initialization. |
| 216 | + */ |
| 217 | + private static void initializeReflection() throws BackupException { |
| 218 | + if (!reflectionInitialized) { |
| 219 | + synchronized (REFLECTION_LOCK) { |
| 220 | + if (!reflectionInitialized) { |
| 221 | + try { |
| 222 | + backupClass = Class.forName("com.arcadedb.integration.backup.Backup"); |
| 223 | + backupConstructor = backupClass.getConstructor(Database.class, String.class); |
| 224 | + setDirectoryMethod = backupClass.getMethod("setDirectory", String.class); |
| 225 | + setVerboseLevelMethod = backupClass.getMethod("setVerboseLevel", Integer.TYPE); |
| 226 | + backupDatabaseMethod = backupClass.getMethod("backupDatabase"); |
| 227 | + reflectionInitialized = true; |
| 228 | + } catch (final ClassNotFoundException | NoSuchMethodException e) { |
| 229 | + throw new BackupException("Backup libs not found in classpath. Make sure arcadedb-integration module is included.", e); |
| 230 | + } |
| 231 | + } |
| 232 | + } |
| 233 | + } |
| 234 | + } |
| 235 | + |
195 | 236 | public String getDatabaseName() { |
196 | 237 | return databaseName; |
197 | 238 | } |
|
0 commit comments