What went wrong?
Kobo sync fails for shelves of more than a few hundred books on slower systems. With 2,051 books on the
Kobo shelf the snapshot takes ~48 s to build, the device gives up after ~45–50 s and closes
the connection, and the next attempt starts from scratch. The user sees only
"Sync failed. Please check your Wi-Fi and try again", which points in entirely the wrong
direction.
Edit from @imnotjames: Cleaned up the what went wrong at least, everything after this is LLM
Root cause
application.yaml asks for batching:
jdbc:
batch_size: 100
order_inserts: true
but the entity defeats it:
// KoboSnapshotBookEntity.java
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Hibernate cannot batch inserts for IDENTITY-generated ids — it has to execute each statement
immediately to read back the generated key — and it disables batching for such entities
without warning. So batch_size: 100 has no effect here, and
KoboLibrarySnapshotService.create() performs one round trip per book.
Observed behaviour
Shelf with 2,051 books, two consecutive attempts:
20:16:07 snapshot created (2,051 books)
20:16:09 SQL_SLOW: insert into kobo_library_snapshot_book (...) 647 ms
20:16:12 SQL_SLOW: ... 1293 ms
20:16:13 SQL_SLOW: ... 978 ms
20:16:14 SQL_SLOW: ... 1102 ms
20:16:16 SQL_SLOW: ... 1469 ms
20:16:17 SQL_SLOW: ... 637 ms
20:16:18 SQL_SLOW: ... 739 ms
20:16:19 SQL_SLOW: ... 909 ms
20:16:20 SQL_SLOW: ... 617 ms
20:16:52 (device retries: second snapshot created)
20:16:55 Request was canceled by client: ServletResponse failed to flushBuffer:
java.io.IOException: Broken pipe
20:17:26 Broken pipe (second attempt)
Both snapshots ended up with all 2,051 rows written and only 161 entitlements marked
synced — i.e. the server finished the work, but far too late for the client.
- 2,051 rows in roughly 48 s ≈ 23 ms per row on average (the lines above are only the
outliers above the 500 ms log_slow_query threshold).
- The same shelf reduced to 61 books syncs cleanly: 61 of 61 delivered, no errors.
- With
batch_size: 100 actually in effect this would be ~21 round trips instead of 2,051.
The database is on a Synology NAS (MariaDB 11.4, --log-bin=binlog --binlog-format=ROW), so
per-statement cost is higher than on a developer machine — but the number of statements is the
part that scales with the shelf, and that is what makes the difference between "works" and
"cannot sync at all".
Suggested fixes
- Let the inserts batch. Replace
GenerationType.IDENTITY with a sequence/table
generator using a pooled optimizer, so the configured batch_size: 100 applies.
- Or drop the surrogate key altogether. The table already carries a natural unique key,
uq_snapshot_book (snapshot_id, book_id). Without a generated id the snapshot could even be
built with a single INSERT ... SELECT on the server.
- Or bypass JPA for this one write with a batched JDBC insert.
- Independently: consider capping or streaming snapshot creation so a large shelf degrades
into a slow-but-successful paged sync instead of a failure loop. Right now every retry
rebuilds the whole snapshot and leaves the previous one behind (see below).
KoboDeletedBookProgressEntity is written with saveAll from
KoboLibrarySnapshotService.getRemovedBooks() and its table also uses an AUTO_INCREMENT id, so
it likely has the same limitation on shelves with many removals.
Side effect: orphaned snapshots
KoboLibrarySyncService only deletes the previous snapshot when a sync completes
(prevSnapshot.ifPresent(sp -> koboLibrarySnapshotService.deleteById(sp.getId()))). Every
failed attempt therefore leaves a full snapshot behind. After the two failures above, the
database held 4,163 rows in kobo_library_snapshot_book of which 4,102 belonged to snapshots
no client would ever reference again. Nothing cleans these up.
Environment
- Grimmory v3.3.3, Java 25.0.4, official Docker image
- MariaDB 11.4 on Synology DSM
- Kobo Libra Colour, firmware 4.x
- 35,091 books; Kobo shelf 2,051 (fails) vs 61 (works)
What went wrong?
Kobo sync fails for shelves of more than a few hundred books on slower systems. With 2,051 books on the
Kobo shelf the snapshot takes ~48 s to build, the device gives up after ~45–50 s and closes
the connection, and the next attempt starts from scratch. The user sees only
"Sync failed. Please check your Wi-Fi and try again", which points in entirely the wrong
direction.
Edit from @imnotjames: Cleaned up the what went wrong at least, everything after this is LLM
Root cause
application.yamlasks for batching:but the entity defeats it:
Hibernate cannot batch inserts for IDENTITY-generated ids — it has to execute each statement
immediately to read back the generated key — and it disables batching for such entities
without warning. So
batch_size: 100has no effect here, andKoboLibrarySnapshotService.create()performs one round trip per book.Observed behaviour
Shelf with 2,051 books, two consecutive attempts:
Both snapshots ended up with all 2,051 rows written and only 161 entitlements marked
synced— i.e. the server finished the work, but far too late for the client.outliers above the 500 ms
log_slow_querythreshold).batch_size: 100actually in effect this would be ~21 round trips instead of 2,051.The database is on a Synology NAS (MariaDB 11.4,
--log-bin=binlog --binlog-format=ROW), soper-statement cost is higher than on a developer machine — but the number of statements is the
part that scales with the shelf, and that is what makes the difference between "works" and
"cannot sync at all".
Suggested fixes
GenerationType.IDENTITYwith a sequence/tablegenerator using a pooled optimizer, so the configured
batch_size: 100applies.uq_snapshot_book (snapshot_id, book_id). Without a generated id the snapshot could even bebuilt with a single
INSERT ... SELECTon the server.into a slow-but-successful paged sync instead of a failure loop. Right now every retry
rebuilds the whole snapshot and leaves the previous one behind (see below).
KoboDeletedBookProgressEntityis written withsaveAllfromKoboLibrarySnapshotService.getRemovedBooks()and its table also uses an AUTO_INCREMENT id, soit likely has the same limitation on shelves with many removals.
Side effect: orphaned snapshots
KoboLibrarySyncServiceonly deletes the previous snapshot when a sync completes(
prevSnapshot.ifPresent(sp -> koboLibrarySnapshotService.deleteById(sp.getId()))). Everyfailed attempt therefore leaves a full snapshot behind. After the two failures above, the
database held 4,163 rows in
kobo_library_snapshot_bookof which 4,102 belonged to snapshotsno client would ever reference again. Nothing cleans these up.
Environment