Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.8.1</string>
<string>0.8.2</string>
<key>CFBundleVersion</key>
<string>9</string>
<string>10</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down
24 changes: 24 additions & 0 deletions Sources/GlossCore/BabelDOCExecutorClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,9 @@ public struct BabelDOCExecutorClient: Sendable {
timeline: timeline,
onProgress: onProgress
)
try await waitForWorkerToFinish(
executionID: created.executionID
)
connection.stateHandler(
.init(
taskID: taskID,
Expand Down Expand Up @@ -876,6 +879,27 @@ public struct BabelDOCExecutorClient: Sendable {
}
}

func waitForWorkerToFinish(
executionID: String,
timeout: Duration = .seconds(30),
pollInterval: Duration = .milliseconds(100)
) async throws {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: timeout)
while true {
let snapshot = try await execution(id: executionID)
if snapshot.workerFinished {
return
}
guard clock.now < deadline else {
throw BabelDOCExecutorError.unavailable(
"PDF worker 完成后未能及时释放执行槽"
)
}
try await Task.sleep(for: pollInterval)
}
}

func waitForCancelledWorker(
executionID: String?,
taskID: String,
Expand Down
52 changes: 50 additions & 2 deletions Tests/GlossCoreTests/BabelDOCExecutorClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ final class BabelDOCExecutorClientTests: XCTestCase {
connection: fixture.connection
),
])
case ("GET", "/v1/executions/execution-1"):
return .json(
Self.executionSnapshot(
executionID: "execution-1",
status: "succeeded"
)
)
default:
return .json(["code": "not_found", "message": "not found"], status: 404)
}
Expand Down Expand Up @@ -321,6 +328,37 @@ final class BabelDOCExecutorClientTests: XCTestCase {
XCTAssertTrue(String(decoding: encodedBody, as: UTF8.self).contains("bridge-secret"))
}

func testTranslationWaitsUntilWorkerReleasesExecutionSlot() async throws {
let fixture = try Fixture()
defer { fixture.remove() }
let attempts = LockedValues<Int>()

StubExecutorURLProtocol.setHandler { request in
guard
request.httpMethod == "GET",
request.url?.path == "/v1/executions/execution-cleanup"
else {
return .json(["code": "not_found", "message": "not found"], status: 404)
}
attempts.append(1)
return .json(
Self.executionSnapshot(
executionID: "execution-cleanup",
status: "succeeded",
workerFinished: attempts.snapshot().count >= 3
)
)
}

try await fixture.client().waitForWorkerToFinish(
executionID: "execution-cleanup",
timeout: .seconds(1),
pollInterval: .milliseconds(1)
)

XCTAssertEqual(attempts.snapshot().count, 3)
}

func testReplayGapRecoversSucceededOutputFromAuthoritativeSnapshot() async throws {
let fixture = try Fixture()
defer { fixture.remove() }
Expand Down Expand Up @@ -365,6 +403,13 @@ final class BabelDOCExecutorClientTests: XCTestCase {
lastSequence: 24
),
], status: 410)
case ("GET", "/v1/executions/execution-gap"):
return .json(
Self.executionSnapshot(
executionID: "execution-gap",
status: "succeeded"
)
)
default:
return .json(["code": "not_found", "message": "not found"], status: 404)
}
Expand Down Expand Up @@ -571,7 +616,8 @@ final class BabelDOCExecutorClientTests: XCTestCase {
taskID: String = "task-1",
initialSequence: Int = 10,
firstAvailableSequence: Int? = 11,
lastSequence: Int = 12
lastSequence: Int = 12,
workerFinished: Bool? = nil
) -> [String: Any] {
[
"execution_id": executionID,
Expand All @@ -582,7 +628,9 @@ final class BabelDOCExecutorClientTests: XCTestCase {
firstAvailableSequence.map { $0 as Any }
?? (NSNull() as Any),
"last_sequence": lastSequence,
"worker_finished": status != "running" && status != "cancelling",
"worker_finished":
workerFinished
?? (status != "running" && status != "cancelling"),
"created_at": 1_000.0,
"finished_at": status == "running" ? NSNull() : 1_001.0,
]
Expand Down
23 changes: 21 additions & 2 deletions Tests/GlossCoreTests/BabelDOCExternalEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ final class BabelDOCExternalEngineTests: XCTestCase {
)
}
let runtime = try XCTUnwrap(BabelDOCExternalEngine.resolveRuntime())
let session = BabelDOCServiceSession()
let stateDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(
"Gloss-BabelDOC-Service-Smoke-\(UUID().uuidString)",
isDirectory: true
)
defer { try? FileManager.default.removeItem(at: stateDirectory) }
let session = BabelDOCServiceSession(
persistedStateDirectoryURL: stateDirectory
)
do {
let baseURL = try await session.start(
runtime: runtime,
Expand Down Expand Up @@ -75,7 +83,18 @@ final class BabelDOCExternalEngineTests: XCTestCase {
withIntermediateDirectories: true
)
let runtime = try XCTUnwrap(BabelDOCExternalEngine.resolveRuntime())
let service = usePersistentLayout ? BabelDOCServiceSession() : nil
let stateDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(
"Gloss-BabelDOC-Benchmark-\(UUID().uuidString)",
isDirectory: true
)
defer { try? FileManager.default.removeItem(at: stateDirectory) }
let service =
usePersistentLayout
? BabelDOCServiceSession(
persistedStateDirectoryURL: stateDirectory
)
: nil
do {
let layoutServiceBaseURL = try await service?.start(
runtime: runtime,
Expand Down
34 changes: 34 additions & 0 deletions docs/release-notes/v0.8.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Gloss 0.8.2

Gloss 0.8.2 makes sequential PDF batches reliable and keeps live service
validation isolated from an already running App session.

## PDF batch reliability

- Waits for BabelDOC's worker cleanup to finish after a successful result
before submitting the next PDF.
- Uses a bounded 30-second handoff timeout with a clear failure instead of
letting the next file hit a transient `busy` response.
- Covers delayed cleanup and replayed terminal results in executor client
tests.

## Service validation

- Gives each opt-in live smoke or benchmark run its own temporary persisted
service state.
- Prevents command-line validation from stopping or replacing the App's
resident PDF executor and layout service.

## Validation

- The full Gloss test suite passes.
- The 15-page *Attention Is All You Need* PDF completed twice in one
persistent session: 15.50 seconds with a fresh layout cache and 6.61 seconds
with a layout cache hit. Both outputs retain selectable text, tables,
formulas, and figures.

## Compatibility

This release is compatible with the signed BabelDOC `0.6.4+gloss.5` runtime.
The runtime also includes a server-side terminal-worker handoff grace period,
so older Gloss clients remain safe while updating.
Loading