Skip to content

Commit 3185220

Browse files
committed
fix(vehicle): keep the stream connected while a drive is interrupted by an API outage
When a fetch reported the vehicle offline or asleep, the stream was always torn down, including mid-drive. There the vehicle is usually only unreachable for the API for a while, and the stream keeps delivering positions; those were discarded until the next successful fetch, leaving a hole in the drive. #5535 already reconnects the stream once the vehicle is back online, but everything streamed in between was lost. Keep the stream while the state machine is in a driving state and record D/N/R frames into the open drive also while it is unavailable or offline. The fetch scheduled by the offline handlers stays the only way back to :available; other frames no longer schedule a fetch in those states, since every one of them would fail the same way.
1 parent cd43e1d commit 3185220

3 files changed

Lines changed: 120 additions & 23 deletions

File tree

lib/teslamate/vehicles/vehicle.ex

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -482,10 +482,24 @@ defmodule TeslaMate.Vehicles.Vehicle do
482482
end
483483
end
484484

485-
{:ok, %Vehicle{state: state} = vehicle} when state in ["offline", "asleep"] ->
485+
{:ok, %Vehicle{state: vehicle_state} = vehicle}
486+
when vehicle_state in ["offline", "asleep"] ->
486487
# disconnect stream in case we started it to detect real online
487488
# (in that case we won't go through Start / :offline or Start / :asleep)
488-
:ok = disconnect_stream(data)
489+
#
490+
# Not while driving: there the vehicle is usually only unreachable for
491+
# the API for a while, and the stream keeps delivering positions that
492+
# belong to the running drive. The driving handlers keep polling and
493+
# decide when the drive is over.
494+
data =
495+
case state do
496+
{:driving, _status, _drive} ->
497+
data
498+
499+
_ ->
500+
:ok = disconnect_stream(data)
501+
%Data{data | stream_pid: nil}
502+
end
489503

490504
%Data{} =
491505
data =
@@ -494,10 +508,10 @@ defmodule TeslaMate.Vehicles.Vehicle do
494508
%Data{data | last_response: last_response, geofence: geofence}
495509
end
496510

497-
{:keep_state, %Data{data | pre_online_check: :idle, stream_pid: nil},
511+
{:keep_state, %Data{data | pre_online_check: :idle},
498512
[
499513
broadcast_fetch(false),
500-
{:next_event, :internal, {:update, {String.to_existing_atom(state), vehicle}}}
514+
{:next_event, :internal, {:update, {String.to_existing_atom(vehicle_state), vehicle}}}
501515
]}
502516

503517
{:ok, %Vehicle{state: state} = vehicle} ->
@@ -718,7 +732,11 @@ defmodule TeslaMate.Vehicles.Vehicle do
718732
%Data{} = data
719733
) do
720734
case {status, stream_data} do
721-
{:available, %Stream.Data{shift_state: shift_state}} when shift_state in ~w(D N R) ->
735+
# Also while the API reports the vehicle unavailable or offline: the
736+
# stream stays connected then, and only a successful fetch brings the
737+
# status back to :available.
738+
{_status, %Stream.Data{shift_state: shift_state}}
739+
when shift_state in ~w(D N R) and not is_nil(drv) ->
722740
{elevation, geofence} =
723741
Repo.checkout(fn ->
724742
{:ok, %{elevation: elevation} = position} =
@@ -740,8 +758,13 @@ defmodule TeslaMate.Vehicles.Vehicle do
740758
geofence: geofence
741759
}, broadcast_summary()}
742760

743-
{_status, %Stream.Data{}} ->
761+
{:available, %Stream.Data{}} ->
744762
{:keep_state_and_data, schedule_fetch(0, data)}
763+
764+
{_status, %Stream.Data{}} ->
765+
# The offline handlers have a fetch scheduled already; one per frame
766+
# would only add vehicle_data requests that are known to fail.
767+
:keep_state_and_data
745768
end
746769
end
747770

test/teslamate/vehicles/vehicle/driving_test.exs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -458,15 +458,16 @@ defmodule TeslaMate.Vehicles.Vehicle.DrivingTest do
458458
assert_receive :continue?
459459
refute_receive _
460460
send(:"api_#{name}", :continue)
461-
assert_receive {:"$websockex_cast", :disconnect}
462461
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :offline}}}
462+
refute_received {:"$websockex_cast", :disconnect}
463463

464464
# Logs previous drive
465465
assert_receive {:close_drive, ^drive, lookup_address: true}, 250
466466

467+
# The stream was kept through the offline period, nothing to reconnect
467468
d1 = DateTime.from_unix!(now_ts + 1 + :timer.minutes(15), :millisecond)
468469
assert_receive {:start_state, ^car, :online, date: ^d1}
469-
assert_receive {ApiMock, {:stream, 1000, _}}
470+
refute_received {ApiMock, {:stream, 1000, _}}
470471
assert_receive {:insert_position, ^car, %{}}
471472
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :online}}}
472473

@@ -537,7 +538,7 @@ defmodule TeslaMate.Vehicles.Vehicle.DrivingTest do
537538
end
538539

539540
@tag :capture_log
540-
test "reconnects the stream when a drive resumes after an offline period",
541+
test "keeps the stream connected through an offline period mid-drive",
541542
%{test: name} do
542543
now = DateTime.utc_now()
543544
now_ts = DateTime.to_unix(now, :millisecond)
@@ -574,14 +575,13 @@ defmodule TeslaMate.Vehicles.Vehicle.DrivingTest do
574575
assert_receive {:insert_position, drive, %{longitude: 0.1, speed: 48}}
575576
assert_receive {:insert_position, ^drive, %{longitude: 0.1, speed: 48}}
576577

577-
# Vehicle goes offline: the stream is disconnected
578-
assert_receive {:"$websockex_cast", :disconnect}
578+
# Vehicle goes offline: the stream stays connected
579579
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :offline}}}, 500
580+
refute_received {:"$websockex_cast", :disconnect}
580581

581-
# Vehicle comes back online mid-drive: the stream is reconnected
582-
assert_receive {ApiMock, {:stream, 1000, _}}, 500
583-
584-
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving}}}
582+
# Vehicle comes back online mid-drive: nothing to reconnect
583+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving}}}, 500
584+
refute_received {ApiMock, {:stream, 1000, _}}
585585
assert_receive {:insert_position, drive, %{longitude: 0.2, speed: 32}}
586586
assert_receive {:insert_position, ^drive, %{longitude: 0.3}}
587587
assert_receive {:close_drive, ^drive, lookup_address: true}
@@ -595,7 +595,7 @@ defmodule TeslaMate.Vehicles.Vehicle.DrivingTest do
595595
end
596596

597597
@tag :capture_log
598-
test "reconnects the stream when a drive resumes after a short unavailability",
598+
test "keeps the stream connected through a short unavailability mid-drive",
599599
%{test: name} do
600600
now = DateTime.utc_now()
601601
now_ts = DateTime.to_unix(now, :millisecond)
@@ -632,13 +632,11 @@ defmodule TeslaMate.Vehicles.Vehicle.DrivingTest do
632632
assert_receive {:insert_position, drive, %{longitude: 0.1, speed: 48}}
633633
assert_receive {:insert_position, ^drive, %{longitude: 0.1, speed: 48}}
634634

635-
# Vehicle becomes unavailable: the stream is disconnected
636-
assert_receive {:"$websockex_cast", :disconnect}
637-
638-
# Vehicle comes back online mid-drive: the stream is reconnected
639-
assert_receive {ApiMock, {:stream, 1000, _}}, 500
640-
641-
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving}}}
635+
# Vehicle becomes unavailable and comes back online mid-drive: the stream
636+
# stays connected, nothing to reconnect
637+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving}}}, 500
638+
refute_received {:"$websockex_cast", :disconnect}
639+
refute_received {ApiMock, {:stream, 1000, _}}
642640
assert_receive {:insert_position, drive, %{longitude: 0.2, speed: 32}}
643641
assert_receive {:insert_position, ^drive, %{longitude: 0.3}}
644642
assert_receive {:close_drive, ^drive, lookup_address: true}

test/teslamate/vehicles/vehicle/streaming_test.exs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,82 @@ defmodule TeslaMate.Vehicles.Vehicle.StreamingTest do
157157
refute_receive _
158158
end
159159

160+
@tag :capture_log
161+
test "keeps recording streamed positions while the vehicle is unavailable for the API",
162+
%{test: name} do
163+
me = self()
164+
now = DateTime.utc_now()
165+
now_ts = DateTime.to_unix(now, :millisecond)
166+
167+
wait_for_continue = fn result ->
168+
fn ->
169+
send(me, :continue?)
170+
171+
receive do
172+
:continue -> result
173+
after
174+
5_000 -> raise "No :continue after 5s"
175+
end
176+
end
177+
end
178+
179+
events = [
180+
{:ok, online_event(now_ts)},
181+
{:ok, online_event(now_ts, drive_state: %{timestamp: now_ts})},
182+
wait_for_continue.({:ok, %TeslaApi.Vehicle{state: "offline"}}),
183+
wait_for_continue.(
184+
{:ok,
185+
online_event(now_ts + 10,
186+
drive_state: %{
187+
timestamp: now_ts + 10,
188+
latitude: 42.5,
189+
longitude: 42.0,
190+
shift_state: "D",
191+
speed: 20,
192+
power: 8
193+
}
194+
)}
195+
),
196+
fn -> Process.sleep(10_000) end
197+
]
198+
199+
:ok = start_vehicle(name, events)
200+
201+
assert_receive {:start_state, car, :online, date: _}
202+
assert_receive {:insert_position, ^car, %{}}
203+
assert_receive {ApiMock, {:stream, _eid, func}} when is_function(func)
204+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :online}}}
205+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :online}}}
206+
207+
assert_receive :continue?
208+
stream(name, %{shift_state: "D", speed: 10, power: 5, time: now})
209+
assert_receive {:start_drive, ^car}
210+
assert_receive {:insert_position, drive, %{speed: 16}}
211+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving, speed: 16}}}
212+
213+
# The API loses the vehicle mid-drive: the stream stays connected
214+
send(:"api_#{name}", :continue)
215+
assert_receive :continue?
216+
refute_received {:"$websockex_cast", :disconnect}
217+
218+
# Streamed positions still belong to the drive
219+
stream(name, %{shift_state: "D", speed: 15, power: 10, est_lat: 42.31, time: now})
220+
assert_receive {:insert_position, ^drive, %{speed: 24, latitude: 42.31}}
221+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving, speed: 24}}}
222+
223+
# Everything else is left to the fetch that is already scheduled
224+
stream(name, %{shift_state: "P", speed: nil, power: nil, time: now})
225+
refute_receive _
226+
227+
# The API sees the vehicle again: the drive continues on the same stream
228+
send(:"api_#{name}", :continue)
229+
assert_receive {:insert_position, ^drive, %{latitude: 42.5, speed: 32}}
230+
assert_receive {:pubsub, {:broadcast, _, _, %Summary{state: :driving}}}
231+
refute_received {ApiMock, {:stream, _, _}}
232+
233+
refute_receive _
234+
end
235+
160236
test "updates the geofence from streaming positions", %{test: name} do
161237
now = DateTime.utc_now()
162238
now_ts = DateTime.to_unix(now, :millisecond)

0 commit comments

Comments
 (0)