Skip to content

Pigeon Task Queues - #117

Open
AlexisChoupault wants to merge 1 commit into
sncf-connect-tech:mainfrom
AlexisChoupault:feature/pigeon-task-queues
Open

Pigeon Task Queues#117
AlexisChoupault wants to merge 1 commit into
sncf-connect-tech:mainfrom
AlexisChoupault:feature/pigeon-task-queues

Conversation

@AlexisChoupault

Copy link
Copy Markdown
Collaborator

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions calendar operations to a serial background thread using Pigeon's task queue feature, removing coroutine scopes in favor of synchronous execution. However, the introduction of synchronous permission checks using CountDownLatch in PermissionHandler.kt poses a severe risk of deadlocks and thread starvation on the serial background queue. Additionally, several methods in CalendarImplem.kt catch specific FlutterError exceptions within generic Exception blocks, resulting in lost error codes (such as NOT_FOUND) being wrapped in GENERIC_ERROR.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +39 to +63
fun requestReadPermissionSync(): Boolean {
var result = false
val latch = CountDownLatch(1)

requestReadPermission { granted ->
result = granted
latch.countDown()
}

latch.await()
return result
}

fun requestWritePermissionSync(): Boolean {
var result = false
val latch = CountDownLatch(1)

requestWritePermission { granted ->
result = granted
latch.countDown()
}

latch.await()
return result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Thread Blocking & Deadlock Risk in Synchronous Permission Requests

Using CountDownLatch to block the calling thread while waiting for an asynchronous OS permission dialog is highly discouraged and introduces several severe risks:

  1. Deadlock Risk: If requestReadPermissionSync or requestWritePermissionSync is ever called on the main thread, the app will deadlock. The main thread will be blocked at latch.await(), preventing activity.runOnUiThread from executing the permission request on the main thread's message loop.
  2. Serial Queue Starvation: Since the Pigeon API is configured to use TaskQueueType.serialBackgroundThread, all calendar operations share a single background thread. Blocking this thread while waiting for user interaction (which can take seconds or minutes) will completely freeze all other calendar operations queued behind it.
  3. Thread Leaks / Indefinite Blocking: If the activity is destroyed, recreated, or the permission request is cancelled by the OS without triggering the callback, the CountDownLatch will never be counted down, blocking the background thread indefinitely.

Recommendation

Keep the permission handling asynchronous. Since Pigeon's @async methods already expect a callback (callback: (Result<T>) -> Unit), you can perform the permission check asynchronously and only invoke the callback once the permission is granted or denied, without blocking any threads.

Comment on lines 834 to +844

} catch (e: Exception) {
callback(
Result.failure(
FlutterError(
code = "GENERIC_ERROR",
message = e.message,
details = e.cause
)
)
} catch (e: Exception) {
callback(
Result.failure(
FlutterError(
code = "GENERIC_ERROR",
message = e.message,
details = e.cause
)
}
}
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Lost Error Code (NOT_FOUND wrapped in GENERIC_ERROR)

When retrieveEvent(eventId) throws a FlutterError (e.g., with code "NOT_FOUND" if the event does not exist), it is caught by the generic catch (e: Exception) block and wrapped in a new FlutterError with code "GENERIC_ERROR". This causes the specific "NOT_FOUND" error code to be lost.

To preserve the specific error code, add a catch block for FlutterError before the generic Exception catch block, similar to how it is done in createEvent and deleteEvent.

        } catch (e: FlutterError) {
            callback(Result.failure(e))
        } catch (e: Exception) {
            callback(
                Result.failure(
                    FlutterError(
                        code = "GENERIC_ERROR",
                        message = e.message,
                        details = e.cause
                    )
                )
            )
        }

Comment on lines +880 to 890
} catch (e: Exception) {
callback(
Result.failure(
FlutterError(
code = "GENERIC_ERROR",
message = e.message,
details = e.cause
)
}
}
)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Lost Error Code (NOT_FOUND wrapped in GENERIC_ERROR)

When retrieveEvent(eventId) throws a FlutterError (e.g., with code "NOT_FOUND" if the event does not exist), it is caught by the generic catch (e: Exception) block and wrapped in a new FlutterError with code "GENERIC_ERROR". This causes the specific "NOT_FOUND" error code to be lost.

To preserve the specific error code, add a catch block for FlutterError before the generic Exception catch block, similar to how it is done in createEvent and deleteEvent.

        } catch (e: FlutterError) {
            callback(Result.failure(e))
        } catch (e: Exception) {
            callback(
                Result.failure(
                    FlutterError(
                        code = "GENERIC_ERROR",
                        message = e.message,
                        details = e.cause
                    )
                )
            )
        }

Comment on lines +926 to 936
} catch (e: Exception) {
callback(
Result.failure(
FlutterError(
code = "GENERIC_ERROR",
message = e.message,
details = e.cause
)
}
}
)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Lost Error Code (NOT_FOUND wrapped in GENERIC_ERROR)

When retrieveEvent(eventId) throws a FlutterError (e.g., with code "NOT_FOUND" if the event does not exist), it is caught by the generic catch (e: Exception) block and wrapped in a new FlutterError with code "GENERIC_ERROR". This causes the specific "NOT_FOUND" error code to be lost.

To preserve the specific error code, add a catch block for FlutterError before the generic Exception catch block, similar to how it is done in createEvent and deleteEvent.

        } catch (e: FlutterError) {
            callback(Result.failure(e))
        } catch (e: Exception) {
            callback(
                Result.failure(
                    FlutterError(
                        code = "GENERIC_ERROR",
                        message = e.message,
                        details = e.cause
                    )
                )
            )
        }

Comment on lines +975 to 985
} catch (e: Exception) {
callback(
Result.failure(
FlutterError(
code = "GENERIC_ERROR",
message = e.message,
details = e.cause
)
}
}
)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Lost Error Code (NOT_FOUND wrapped in GENERIC_ERROR)

When retrieveEvent(eventId) throws a FlutterError (e.g., with code "NOT_FOUND" if the event does not exist), it is caught by the generic catch (e: Exception) block and wrapped in a new FlutterError with code "GENERIC_ERROR". This causes the specific "NOT_FOUND" error code to be lost.

To preserve the specific error code, add a catch block for FlutterError before the generic Exception catch block, similar to how it is done in createEvent and deleteEvent.

        } catch (e: FlutterError) {
            callback(Result.failure(e))
        } catch (e: Exception) {
            callback(
                Result.failure(
                    FlutterError(
                        code = "GENERIC_ERROR",
                        message = e.message,
                        details = e.cause
                    )
                )
            )
        }

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.57%. Comparing base (2912b2a) to head (3119078).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #117      +/-   ##
============================================
+ Coverage     88.09%   93.57%   +5.48%     
============================================
  Files            23        8      -15     
  Lines          4207     2818    -1389     
  Branches        112        0     -112     
============================================
- Hits           3706     2637    -1069     
+ Misses          422      181     -241     
+ Partials         79        0      -79     
Flag Coverage Δ
unittests 93.57% <ø> (+5.48%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant