-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Leverage structured concurrency for connection handling #749
Conversation
2115401
to
f77ffa7
Compare
bf4d2a5
to
565f307
Compare
c5dde43
to
602390a
Compare
602390a
to
cab1cab
Compare
} catch (e: Exception) { | ||
closeConnection() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We no longer have to explicitly close the connection, because it will be closed implicitly when the connection coroutine scope tree completes. Since the connection process runs within a Deferred
that is a child of the connection coroutine scope tree, any failure during connect will implicit close down the Connection
.
throw failure | ||
} | ||
|
||
logger.info { message = "Connected" } | ||
_state.value = State.Connected | ||
|
||
state.watchForConnectionLossIn(scope) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is performed in the Connection
now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought there were going to be changes to didUpdateState here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow. Can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was referring to the flow that tells the consumer the BLE state. Doesn't sound like it's happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps there was confusion re: removal of the connectionState
flow?
There was previously connectionEvents
and connectionState
, I decided that having a dedicated flow (connectionState
) for the connection events was unnecessary, and now they go through connectionEvents
(along with the other connection events).
/** | ||
* @throws UnmetRequirementException If [CentralManager] state is not [CBManagerStatePoweredOn]. | ||
*/ | ||
internal fun CentralManager.checkBluetoothIsOn() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this file would be the right place, but it would be really useful to have a flow of the CBManagerState itself. Is that factored into your plan somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, I've decided that Kable should not provide real-time (i.e. flow based) bluetooth state for consumers, at least not in the core
module (perhaps a separate module or library 🤷) — it is too difficult to provide a unified API.
I'm trying to drive Kable in a direction where the entire API can be used from common
source sets (write once, use everywhere paradigm).
In this particular instance: CBManager*
states encompass:
- is BLE supported or not
- is BLE powered on or not
- does user have permission to use BLE
Whereas Android has the three above bullets available independently (and there is no live-updating permission info available on Android — it is on-demand only).
If CBManager*
states didn't include the permission aspect, then this would be trivial to provide a unified API (of a flow of "is BLE powered on or not"), but since Apple doesn't separate those states, means that I just can't provide such an API.
So, we're left with only being able to provide an on-demand approach. Which sadly, I may even need to abandon this; as I've found that my previous findings of "being able to suppress the dialog when using BLE" isn't actually possible on Apple (I was previously mistaken on my findings: I am able to suppress for BLE on/off dialog, but permissions always comes up — see also: What causes this iOS permission prompt for "use Bluetooth for new connections"?). See also: #752
The Apple BLE dialog makes this API super intrusive...ultimately, it is hot garbage that Core Bluetooth doesn't let you query any information about BLE availability without first showing the user a dialog. 😢 You can't even check if BLE is supported w/o showing a dialog!! 🤦
I haven't completely abandoned the idea, but a unified API for these things may be impossible; really just trying to find the best compromise (and still have a reasonably friendly API).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional details can also be found here: #737
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty awesome, much easier to reason about the lifecycle of the various moving parts now. 🚀
// todo: Move this `require` to the PeripheralBuilder. | ||
require(disconnectTimeout > ZERO) { "Disconnect timeout must be >0, was $disconnectTimeout" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this TODO dependent on some other work or can this be moved to PeripheralBuilder
as part of this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget why I didn't do it in this PR. I'll try again. If I can't move it quite yet, then I'll merge w/ it as-is (with the todo
) and try to get to it soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked into it, I could move this, but it'd be duplicated for each platform (which is expected), but I realized that I plan to do some refactoring of PeripheralBuilder
(so that Config
objects are created, to make passing of those values around easier). I figured I'd just wait for that small refactor to pull this in. 🤷
Summary
Major changes include:
SharedRepeatableAction
SharedRepeatableAction
(for triggering connect/disconnect)Peripheral
s are no longer children of aCoroutineScope
cancel
Peripheral
s are themselvesCoroutineScope
slaunch
ed fromPeripheral
s when the lifecycle of the task is intended to match thePeripheral
s lifecyclePeripheral.connect
now returns aCoroutineScope
CoroutineScope
IOException
orIllegalStateException
for most exceptions (Restructure exception hierarchy #724)Scoped connections
Connection and I/O management, as well as disconnect process was moved to
Connection
class (for every platform). TheConnection
class manages any context switching necessary for I/O and ensuring proper teardown on disconnect. This simplifies connection lifecycle management by leveraging coroutine's structured concurrency for connection lifecycle (proper teardown on either disconnect or peripheral scope completing).We no longer have to do a
Deferred
dance (state management) when performing I/O (when cancelled by caller). When I/O goes throughConnection.execute
(which is scoped to theConnection
'sCoroutineScope
), if caller cancels I/O request, then we can continue (asynchronously) waiting for the response (rather than trying to drop the response on the next I/O operation).The following chart shows the approximate relationship between the various
CoroutineScope
s within aPeripheral
:When
Peripheral.connect
is called, theCoroutineScope
tree (shown in pink) is spun up (viaSharedRepeatableAction
). TheSharedRepeatableAction
creates aCoroutineScope
(which becomes theConnection
'sconnectionScope
) and a childDeferred
(which is executed as anaction
within thePeripheral.connect
function). In theaction
, theConnection
is created (using theconnectionScope
) and immediately a childCoroutineScope
(which is shown astaskScope
) is created.In summary:
connectionScope
is used for asynchronously executing I/O operationstaskScope
is provided to library consumers to launch "tasks" that should match the lifecycle of the connectionaction
runs the connection process (e.g. connect, discover services, etc)Some characteristics of this
CoroutineScope
tree that are pertinent:connectionScope
will shutdown the connectionCoroutineScope
tree (pink elements)taskScope
) are allowed to fail w/o shutting anything else downaction
(during connect process) will shutdown the connectionCoroutineScope
tree (pink elements)When
Connection
is created, it registers aJob
completion listener on theconnectionScope
which properly disposes of the underlying peripheral (e.g. on Android, theBluetoothGatt
isclose
d) — this ensures that after the connectionCoroutineScope
tree has completed that the underlying BLE objects are always properly disposed.Partially addresses #724
Partially addresses #721
Closes #622
Closes #567
Closes #378
Closes #177
Closes #144
Closes #497
Closes #286
Closes #168