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
2 changes: 1 addition & 1 deletion docs/architecture/arc42-CanKit.Pro.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ classDiagram
+ReceiveAsync(ct) Task~byte[]~
+ReceiveWithArrivalAsync(ct) Task~IsoTpReceivedPdu~
+ReceiveAllAsync(ct) IAsyncEnumerable~byte[]~
+SettleAsync() Task
+SettleAsync(ct) Task
+event DatagramReceived
+event BackgroundExceptionOccurred
}
Expand Down
9 changes: 8 additions & 1 deletion src/CanKit.Pro.IsoTp/IIsoTpChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,15 @@ Task<IsoTpTransmitStamps> SendWithTransmitStampAsync(ReadOnlyMemory<byte> pdu,
/// channel's own actor — from a <c>BackgroundExceptionOccurred</c> handler — it returns at
/// once and the frames on their way are handled after the current work item: they queue
/// behind frames already in the mailbox, and handling them inline would reorder the two.
/// <para>
/// <paramref name="cancellationToken"/> ends the wait, not the settling: what the demux had
/// buffered has been handed to the actor by then and is handled as usual. A token cancelled
/// before the call, or while the wait is still queued behind the actor's other work, cancels
/// the returned task; once the actor has reached the wait it completes normally.
/// </para>
/// </summary>
Task SettleAsync();
/// <param name="cancellationToken">Ends the wait for the actor to catch up.</param>
Task SettleAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Drains every buffered inbox item — both completed PDUs and pending reassembly-abort
Expand Down
5 changes: 2 additions & 3 deletions src/CanKit.Pro.IsoTp/IsoTpChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,7 @@ public bool TryReceiveWithArrival(out IsoTpReceivedPdu pdu)
}

/// <inheritdoc />
/// <inheritdoc />
public Task SettleAsync()
public Task SettleAsync(CancellationToken cancellationToken = default)
{
if (Volatile.Read(ref _disposed) != 0) return Task.CompletedTask;
// What the demux has buffered goes to the actor now rather than after the reader's
Expand All @@ -372,7 +371,7 @@ public Task SettleAsync()
if (_actor is ProtocolActor { IsOnCurrentActor: true }) return Task.CompletedTask;
try
{
return _actor.PostAsync(() => { });
return _actor.PostAsync(() => { }, cancellationToken);
}
catch (ObjectDisposedException)
{
Expand Down
3 changes: 2 additions & 1 deletion src/CanKit.Pro.IsoTp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ CAN-FD long-payload cases still get the least coverage of the two halves.
`SettleAsync` drains it the same way and completes once the actor has taken everything
queued so far, without dropping anything: for a decision taken at a deadline, what the inbox
does not hold after it did not arrive before the call — a Single Frame stamped in time can
otherwise still be on its way when the deadline fires. `DiscardPendingPdus(long)` drops what
otherwise still be on its way when the deadline fires. A `CancellationToken` ends the wait,
not the settling: what the demux buffered has been handed to the actor by then. `DiscardPendingPdus(long)` drops what
arrived before the caller's own stamp rather than before now, so a caller that reads the
inbox after taking the stamp and discards after reading has seen everything it drops, and a
frame from between the read and the discard is kept.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace CanKit.Pro.IsoTp
System.Threading.Tasks.Task<CanKit.Pro.IsoTp.IsoTpReceivedPdu> ReceiveWithArrivalAsync(System.Threading.CancellationToken cancellationToken = default);
System.Threading.Tasks.Task SendAsync(System.ReadOnlyMemory<byte> pdu, System.Threading.CancellationToken cancellationToken = default);
System.Threading.Tasks.Task<CanKit.Pro.IsoTp.IsoTpTransmitStamps> SendWithTransmitStampAsync(System.ReadOnlyMemory<byte> pdu, System.Threading.CancellationToken cancellationToken = default);
System.Threading.Tasks.Task SettleAsync();
System.Threading.Tasks.Task SettleAsync(System.Threading.CancellationToken cancellationToken = default);
bool TryReceiveWithArrival(out CanKit.Pro.IsoTp.IsoTpReceivedPdu pdu);
}
public static class IsoTp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,67 @@ public async Task Settle_Takes_A_Buffered_Single_Frame_Through_To_The_Inbox()
pdu.FirstFrameArrivalTimestamp.Should().Be(arrival, "the stamp is the demux's, not the settle's");
}

// The token ends the wait for the actor, not the settling: a caller that gives up on the wait
// (its own deadline passed, its operation was cancelled) is not held behind whatever the
// channel's actor is busy with.
[Fact]
public async Task Settle_With_An_Already_Cancelled_Token_Is_Cancelled()
{
var ep = IsoTpEndpoint.Normal(txCanId: 0x7E0, rxCanId: 0x7E8);
using var service = new StarvedReaderBusService();
using var actor = new ProtocolActor();
using var channel = new IsoTpChannel(service, ep, FastOptions(), ownsService: false, actor);
using var cts = new CancellationTokenSource();
cts.Cancel();

var ex = await Record.ExceptionAsync(() => channel.SettleAsync(cts.Token).WaitAsync(ShortTimeout));

ex.Should().BeAssignableTo<OperationCanceledException>();
}

[Fact]
public async Task Settle_Cancelled_While_The_Actor_Is_Busy_Releases_The_Caller_At_Once()
{
var ep = IsoTpEndpoint.Normal(txCanId: 0x7E0, rxCanId: 0x7E8);
using var service = new StarvedReaderBusService();
using var actor = new ProtocolActor(ActorExecutionMode.DedicatedThread);
using var channel = new IsoTpChannel(service, ep, FastOptions(), ownsService: false, actor);
using var release = new ManualResetEventSlim();
using var busy = new ManualResetEventSlim();
actor.Post(() => { busy.Set(); release.Wait(); });
busy.Wait(ShortTimeout).Should().BeTrue("the actor must be inside the blocking item");

using var cts = new CancellationTokenSource();
var settle = channel.SettleAsync(cts.Token);
settle.IsCompleted.Should().BeFalse("the settle waits behind the item the actor is busy with");

cts.Cancel();

// Released while the actor is still blocked: not when it eventually gets to the wait.
var ex = await Record.ExceptionAsync(() => settle.WaitAsync(ShortTimeout));
ex.Should().BeAssignableTo<OperationCanceledException>();

release.Set();
}

[Fact]
public async Task Settle_With_A_Live_Token_Takes_A_Buffered_Frame_Through_Like_Without_One()
{
var ep = IsoTpEndpoint.Normal(txCanId: 0x7E0, rxCanId: 0x7E8);
using var service = new StarvedReaderBusService();
using var actor = new ProtocolActor();
using var channel = new IsoTpChannel(service, ep, FastOptions(), ownsService: false, actor);
using var cts = new CancellationTokenSource();

byte[] sf = { 0x03, 0x7F, 0x3E, 0x78, 0x00, 0x00, 0x00, 0x00 };
service.Deliver(new CanFrameView(CanFrameType.Can20, 0x7E8, sf, FrameFlags.None), Stopwatch.GetTimestamp());

await channel.SettleAsync(cts.Token).WaitAsync(ShortTimeout);

channel.TryReceiveWithArrival(out var pdu).Should().BeTrue("settling took the frame through");
pdu.Pdu.Should().Equal(0x7F, 0x3E, 0x78);
}

// Codex on #150: a discard given the caller's stamp drops what arrived before it and keeps
// what arrived since -- so a caller that read the inbox after taking the stamp has seen
// everything the discard drops, and a frame from between the read and the discard is not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ private long Ticks(TimeSpan span)
public async Task<byte[]> ReceiveAsync(CancellationToken cancellationToken = default)
=> (await ReceiveWithArrivalAsync(cancellationToken).ConfigureAwait(false)).Pdu;

public Task SettleAsync() => Task.CompletedTask; // nothing is ever on its way: the stub is its own actor
public Task SettleAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; // nothing is ever on its way: the stub is its own actor

public int DiscardPendingPdus() => 0;

Expand Down
Loading