Skip to content

Commit d17ea76

Browse files
feat: saas migration improvements
1 parent 458cd44 commit d17ea76

6 files changed

Lines changed: 97 additions & 46 deletions

File tree

extensions/saas/sources/migration/src/main/java/tools/dynamia/modules/saas/migration/pipeline/ExportPipeline.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public void export(Serializable accountId,
175175
List<Class<?>> candidates = discovery.discoverExportableEntities();
176176
if (options.getEntities() != null && !options.getEntities().isEmpty()) {
177177
candidates.removeIf(c -> !options.getEntities().contains(c.getSimpleName()));
178-
logger.info("[Migration/Export] Filtered entities, {} remaining", candidates.size());
178+
logger.info("[Migration/Export] Account {}: Filtered entities, {} remaining", accountId, candidates.size());
179179
}
180180

181181
List<Class<?>> ordered = dependencyGraph.topologicalSort(candidates);
@@ -203,7 +203,7 @@ public void export(Serializable accountId,
203203
// ── 3. Zip temp dir → output (topological order) ──────────────────
204204
if (token == null || !token.isCancelled()) {
205205
zipToOutput(tempDir, ordered, accountId, output);
206-
logger.info("[Migration/Export] ZIP written successfully for accountId={}", accountId);
206+
logger.info("[Migration/Export] ZIP written successfully for accountId={}", accountId);
207207

208208

209209
}
@@ -318,8 +318,8 @@ private void exportEntitiesInParallel(Path tempDir,
318318
}
319319
}
320320

321-
logger.info("[Migration/Export] All entities exported — {} types, {} total records",
322-
processedTypes.get(), totalRecords.get());
321+
logger.info("[Migration/Export] Account {}: All entities exported — {} types, {} total records",
322+
accountId, processedTypes.get(), totalRecords.get());
323323
}
324324

325325
// ─────────────────────────────────────────────────────────────────────────
@@ -354,7 +354,7 @@ private long exportEntityToFile(Path tempDir, Class<?> entityClass,
354354
localEm.close();
355355

356356

357-
logger.info("[Migration/Export] {} with {} columns", entityClass.getSimpleName(), columns.size());
357+
logger.info("[Migration/Export] Account {}: {} with {} columns", accountId, entityClass.getSimpleName(), columns.size());
358358
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(filePath), ENTITY_BUFFER_SIZE);
359359
JsonGenerator gen = objectMapper.createGenerator(out)) {
360360

@@ -377,7 +377,7 @@ private long exportEntityToFile(Path tempDir, Class<?> entityClass,
377377

378378
gen.writeEndObject();
379379

380-
logger.info("[Migration/Export] {} → {} records written in {}ms", entityClass.getSimpleName(), processed, (endTime - startTime));
380+
logger.info("[Migration/Export] Account {}: {} → {} records written in {}ms", accountId, entityClass.getSimpleName(), processed, (endTime - startTime));
381381
return processed;
382382
}
383383
} finally {
@@ -448,8 +448,8 @@ private long writeEntityRows(JsonGenerator gen,
448448
}
449449
long endTime = System.currentTimeMillis();
450450

451-
logger.info("[Migration/Export] {} - page {} with {} records. Query={}ms Write={}ms. Rows={} ",
452-
simpleName, pageNum, page.size(), (qEndTime - qStartTime), (endTime - startTime), processed);
451+
logger.info("[Migration/Export] Account {}: {} - page {} with {} records. Query={}ms Write={}ms. Rows={} ",
452+
accountId, simpleName, pageNum, page.size(), (qEndTime - qStartTime), (endTime - startTime), processed);
453453

454454
if (listener != null) {
455455
listener.onProgress(MigrationProgress.partial(processed));

extensions/saas/sources/migration/src/main/java/tools/dynamia/modules/saas/migration/services/AccountMigrationJobServiceImpl.java

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.springframework.core.io.InputStreamSource;
1414
import tools.dynamia.commons.logger.LoggingService;
1515
import tools.dynamia.domain.query.QueryConditions;
16+
import tools.dynamia.domain.util.QueryBuilder;
1617
import tools.dynamia.modules.saas.migration.api.MigrationProgressListener;
1718
import tools.dynamia.navigation.Page;
1819
import tools.jackson.databind.ObjectMapper;
@@ -49,9 +50,11 @@
4950
import java.nio.file.StandardCopyOption;
5051
import java.time.LocalDateTime;
5152
import java.time.format.DateTimeFormatter;
53+
import java.util.HashMap;
5254
import java.util.List;
5355
import java.util.Map;
5456
import java.util.concurrent.ConcurrentHashMap;
57+
import java.util.concurrent.ExecutionException;
5558
import java.util.concurrent.Semaphore;
5659
import java.util.concurrent.atomic.AtomicBoolean;
5760
import java.util.concurrent.atomic.AtomicLong;
@@ -76,7 +79,7 @@ public class AccountMigrationJobServiceImpl implements AccountMigrationJobServic
7679

7780
private static final LoggingService log = LoggingService.get(AccountMigrationJobServiceImpl.class);
7881
private static final DateTimeFormatter FILE_TS = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss");
79-
private static final long PROGRESS_THROTTLE_MS = 2000;
82+
private static final long PROGRESS_THROTTLE_MS = 8000;
8083

8184
/**
8285
* In-memory token registry: jobUuid → CancellationToken. Cleaned up when job finishes.
@@ -336,27 +339,51 @@ private void finalizeJob(String jobUuid, Throwable ex, Path resultFile, Cancella
336339
private MigrationProgressListener buildProgressListener(AccountMigrationJob job) {
337340
AtomicBoolean started = new AtomicBoolean(false);
338341
AtomicLong lastPersistedAt = new AtomicLong(0);
342+
AtomicLong partialSum = new AtomicLong(0);
339343

340344
return (MigrationProgress p) -> {
341345
boolean isFirst = started.compareAndSet(false, true);
342-
long now = System.currentTimeMillis();
343-
boolean throttleExpired = (now - lastPersistedAt.get()) >= PROGRESS_THROTTLE_MS;
344-
boolean isFinal = p.totalEntities() > 0 && p.processedEntities() >= p.totalEntities();
345346

346-
if (!isFirst && !throttleExpired && !isFinal) return;
347+
if (isFirst || !p.partial()) {
348+
try {
349+
crudService.executeWithinTransaction(() -> {
350+
AccountMigrationJob j = findByUuid(job.getUuid());
351+
if (j != null && !j.isFinished()) {
352+
Map<String, Object> fields = new HashMap<>();
353+
if (isFirst) {
354+
j.markRunning();
355+
fields.put("status", j.getStatus());
356+
fields.put("startedAt", j.getStartedAt());
357+
}
358+
359+
fields.put("progress", p.percentage());
360+
fields.put("progressMessage", p.message());
361+
fields.put("records", p.processedRecords());
362+
crudService.batchUpdate(AccountMigrationJob.class, fields, QueryParameters.with("id", job.getId()));
363+
}
364+
});
365+
} catch (Exception e) {
366+
log.debug("[Migration/Jobs] Progress update error for {}: {}", job.getUuid(), e.getMessage());
367+
}
368+
} else {
369+
long now = System.currentTimeMillis();
370+
boolean throttleExpired = (now - lastPersistedAt.get()) >= PROGRESS_THROTTLE_MS;
371+
boolean isFinal = p.totalEntities() > 0 && p.processedEntities() >= p.totalEntities();
372+
partialSum.addAndGet(p.processedRecords());
347373

348-
lastPersistedAt.set(now);
349-
try {
350-
crudService.executeWithinTransaction(() -> {
351-
AccountMigrationJob j = findByUuid(job.getUuid());
352-
if (j != null && !j.isFinished()) {
353-
if (isFirst) j.markRunning();
354-
j.updateProgress(p);
355-
crudService.update(j);
356-
}
357-
});
358-
} catch (Exception e) {
359-
log.debug("[Migration/Jobs] Progress update error for {}: {}", job.getUuid(), e.getMessage());
374+
if (!throttleExpired && !isFinal) return;
375+
376+
lastPersistedAt.set(now);
377+
try {
378+
var sum = partialSum.getAndSet(0);
379+
crudService.executeWithinTransaction(() -> {
380+
crudService.execute("UPDATE AccountMigrationJob j SET j.records = j.records + :partial where j.id = :id", QueryParameters.with("id", job.getId())
381+
.add("partial", sum));
382+
});
383+
384+
} catch (Exception e) {
385+
log.debug("[Migration/Jobs] Progress update error for {}: {}", job.getUuid(), e.getMessage());
386+
}
360387
}
361388
};
362389
}

extensions/saas/sources/ui/src/main/resources/META-INF/descriptors/AccountEntityPicker.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ autofields: false
44

55
fields:
66
name:
7+
subdomain:
78
contact:
89
identification:
910
type:

platform/app/src/main/java/tools/dynamia/app/DynamiaToolsWebApplication.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,4 @@ public PWAManifest defaultManifest(ApplicationInfo applicationInfo) {
7272
.build();
7373
}
7474

75-
@Bean
76-
public PWAManifestController pwaManifestController(PWAManifest manifest) {
77-
return new PWAManifestController(manifest);
78-
}
79-
80-
8175
}
Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package tools.dynamia.app.controllers;
22

3+
import org.springframework.http.ResponseEntity;
34
import org.springframework.web.bind.annotation.GetMapping;
4-
import org.springframework.web.bind.annotation.ResponseBody;
55
import org.springframework.web.bind.annotation.RestController;
6+
import tools.dynamia.integration.Containers;
67
import tools.dynamia.web.pwa.PWAManifest;
78

89
/**
@@ -14,36 +15,31 @@
1415
* <ul>
1516
* <li>GET /manifest.json - Returns the PWA manifest</li>
1617
* </ul>
17-
*
18+
* <p>
1819
* When needed register using @Bean in a configuration class:
1920
*
2021
* @author Mario A. Serrano Leones
2122
* @since 2023
2223
*/
23-
@ResponseBody
24+
@RestController
2425
public class PWAManifestController {
2526

2627
/**
2728
* The PWA manifest instance to be served.
2829
*/
29-
private final PWAManifest manifest;
30-
31-
/**
32-
* Constructs a new {@code PWAManifestController} with the given manifest.
33-
*
34-
* @param manifest the PWA manifest instance
35-
*/
36-
public PWAManifestController(PWAManifest manifest) {
37-
this.manifest = manifest;
38-
}
30+
private PWAManifest manifest;
3931

4032
/**
4133
* Returns the PWA manifest as JSON.
4234
*
4335
* @return the {@link PWAManifest} object
4436
*/
4537
@GetMapping(value = "/manifest.json", produces = "application/json")
46-
public PWAManifest getManifest() {
47-
return manifest;
38+
public ResponseEntity<PWAManifest> getManifest() {
39+
if (manifest == null) {
40+
manifest = Containers.get().findObject(PWAManifest.class);
41+
}
42+
43+
return manifest != null ? ResponseEntity.ok(manifest) : ResponseEntity.notFound().build();
4844
}
4945
}

platform/core/commons/src/main/java/tools/dynamia/commons/StringUtils.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,7 @@ public static String getSystemPropertyOrEnv(String name) {
14851485

14861486
/**
14871487
* Truncate string to max lenght
1488+
*
14881489
* @param text
14891490
* @param maxlength
14901491
* @return
@@ -1499,6 +1500,7 @@ public static String truncate(String text, int maxlength) {
14991500

15001501
/**
15011502
* Remove special characters from string
1503+
*
15021504
* @param input
15031505
* @return
15041506
*/
@@ -1514,4 +1516,35 @@ public static String replaceSpecialCharacters(String input) {
15141516

15151517
return withoutAccents;
15161518
}
1519+
1520+
/**
1521+
* Takes characters from a string.
1522+
* <p>
1523+
* length > 0 -> first N characters
1524+
* length < 0 -> last N characters
1525+
* length == 0 -> empty string
1526+
*/
1527+
public static String take(String str, int length) {
1528+
if (str == null) {
1529+
return null;
1530+
}
1531+
1532+
if (length == 0) {
1533+
return "";
1534+
}
1535+
1536+
int strLength = str.length();
1537+
1538+
if (length > 0) {
1539+
return strLength <= length
1540+
? str
1541+
: str.substring(0, length);
1542+
}
1543+
1544+
int abs = Math.abs(length);
1545+
1546+
return strLength <= abs
1547+
? str
1548+
: str.substring(strLength - abs);
1549+
}
15171550
}

0 commit comments

Comments
 (0)