Skip to content

Commit 6bd720d

Browse files
authored
Merge pull request #1129 from noaccOS/refactor/datetimes
refactor(appengine): use datetimes for data insertion
2 parents 80a3450 + cc6f174 commit 6bd720d

File tree

6 files changed

+163
-70
lines changed

6 files changed

+163
-70
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.AppEngine.API.DateTime do
20+
@moduledoc """
21+
Ecto type for DateTimes with millisecond precision.
22+
"""
23+
use Ecto.Type
24+
25+
@type t :: DateTime.t()
26+
27+
@doc false
28+
def type, do: :utc_datetime_msec
29+
30+
@spec load(t() | any()) :: {:ok, t()} | :error
31+
def load(%DateTime{} = datetime), do: {:ok, datetime}
32+
def load(timestamp) when is_integer(timestamp), do: {:ok, DateTime.from_unix!(timestamp)}
33+
def load(_other), do: :error
34+
35+
# xandra accepts both integers and datetimes, it can do the job for us
36+
@spec dump(t() | any()) :: {:ok, t()} | :error
37+
def dump(%DateTime{} = datetime), do: {:ok, datetime}
38+
def dump(timestamp) when is_integer(timestamp), do: {:ok, timestamp}
39+
def dump(_other), do: :error
40+
41+
@spec cast(t() | any()) :: {:ok, t()} | :error
42+
def cast(datetime_or_timestamp_or_any), do: load(datetime_or_timestamp_or_any)
43+
44+
def split_submillis(timestamp) do
45+
timestamp_ms = DateTime.truncate(timestamp, :millisecond)
46+
submillis = timestamp |> DateTime.to_unix(:microsecond) |> rem(1000)
47+
# `DateTime`s are microsecond precision, individual_properties's submillis
48+
# have one extra digit. Keep the unit consistent with DUP
49+
decimicrosecond_submillis = submillis * 10
50+
51+
{timestamp_ms, decimicrosecond_submillis}
52+
end
53+
end

apps/astarte_appengine_api/lib/astarte_appengine_api/device/device.ex

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,6 @@ defmodule Astarte.AppEngine.API.Device do
221221

222222
now = DateTime.utc_now()
223223

224-
timestamp_micro =
225-
now
226-
|> DateTime.to_unix(:microsecond)
227-
228224
db_max_ttl =
229225
if mapping.database_retention_policy == :use_ttl do
230226
min(realm_max_ttl, mapping.database_retention_ttl)
@@ -249,7 +245,7 @@ defmodule Astarte.AppEngine.API.Device do
249245
mapping,
250246
path,
251247
value,
252-
timestamp_micro,
248+
now,
253249
opts
254250
)
255251

@@ -376,10 +372,6 @@ defmodule Astarte.AppEngine.API.Device do
376372
) do
377373
now = DateTime.utc_now()
378374

379-
timestamp_micro =
380-
now
381-
|> DateTime.to_unix(:microsecond)
382-
383375
with {:ok, mappings} <-
384376
Mappings.fetch_interface_mappings(
385377
realm_name,
@@ -425,7 +417,7 @@ defmodule Astarte.AppEngine.API.Device do
425417
nil,
426418
path,
427419
value,
428-
timestamp_micro,
420+
now,
429421
opts
430422
)
431423

apps/astarte_appengine_api/lib/astarte_appengine_api/device/queries.ex

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ defmodule Astarte.AppEngine.API.Device.Queries do
2020
import Ecto.Query
2121

2222
alias Astarte.AppEngine.API.Config
23+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
2324
alias Astarte.AppEngine.API.Device.DeletionInProgress
2425
alias Astarte.AppEngine.API.Device.DeviceStatus
2526
alias Astarte.AppEngine.API.Device.DevicesList
@@ -181,19 +182,17 @@ defmodule Astarte.AppEngine.API.Device.Queries do
181182

182183
keyspace = DataAccessRealm.keyspace_name(realm_name)
183184

184-
# Ecto expects microsecond precision
185-
{reception, reception_submillis} = split_datetime_to_ms_and_submillis(reception_timestamp)
186-
value_timestamp = value_timestamp |> DateTime.truncate(:millisecond) |> pad_usec()
185+
value =
186+
%IndividualProperty{
187+
device_id: device_id,
188+
interface_id: interface_descriptor.interface_id,
189+
endpoint_id: endpoint_id,
190+
path: path,
191+
reception: reception_timestamp,
192+
datetime_value: value_timestamp
193+
}
187194

188-
value = %IndividualProperty{
189-
device_id: device_id,
190-
interface_id: interface_descriptor.interface_id,
191-
endpoint_id: endpoint_id,
192-
path: path,
193-
reception_timestamp: reception,
194-
reception_timestamp_submillis: reception_submillis,
195-
datetime_value: value_timestamp
196-
}
195+
value = value |> IndividualProperty.prepare_for_db()
197196

198197
ttl = opts[:ttl]
199198

@@ -262,7 +261,7 @@ defmodule Astarte.AppEngine.API.Device.Queries do
262261
value_column = CQLUtils.type_to_db_column_name(endpoint.value_type)
263262
keyspace = DataAccessRealm.keyspace_name(realm_name)
264263

265-
{timestamp_ms, timestamp_submillis} = split_ms_and_submillis(timestamp)
264+
{timestamp_ms, timestamp_submillis} = DateTimeMs.split_submillis(timestamp)
266265

267266
# TODO: :reception_timestamp_submillis is just a place holder right now
268267
interface_storage_attributes = %{
@@ -299,7 +298,7 @@ defmodule Astarte.AppEngine.API.Device.Queries do
299298
) do
300299
value_column = CQLUtils.type_to_db_column_name(endpoint.value_type)
301300
keyspace = DataAccessRealm.keyspace_name(realm_name)
302-
{timestamp_ms, timestamp_submillis} = split_ms_and_submillis(timestamp)
301+
{timestamp_ms, timestamp_submillis} = DateTimeMs.split_submillis(timestamp)
303302

304303
attributes = %{
305304
value_column => to_db_friendly_type(value),
@@ -312,7 +311,12 @@ defmodule Astarte.AppEngine.API.Device.Queries do
312311
reception_timestamp_submillis: timestamp_submillis
313312
}
314313

315-
Repo.insert_all(interface_descriptor.storage, [attributes], prefix: keyspace, ttl: opts[:ttl])
314+
{1, _} =
315+
Repo.insert_all(interface_descriptor.storage, [attributes],
316+
prefix: keyspace,
317+
ttl: opts[:ttl]
318+
)
319+
316320
:ok
317321
end
318322

@@ -348,14 +352,12 @@ defmodule Astarte.AppEngine.API.Device.Queries do
348352
{endpoint_name, %{name: column_name, type: endpoint.value_type}}
349353
end)
350354

351-
{timestamp_ms, submillis} = split_ms_and_submillis(timestamp)
352-
353355
base_attributes = %{
354356
device_id: device_id,
355357
path: path
356358
}
357359

358-
timestamp_attributes = timestamp_attributes(explicit_timestamp?, timestamp_ms, submillis)
360+
timestamp_attributes = timestamp_attributes(explicit_timestamp?, timestamp)
359361
value_attributes = value_attributes(column_meta, value)
360362

361363
object_datastream =
@@ -372,15 +374,21 @@ defmodule Astarte.AppEngine.API.Device.Queries do
372374
:ok
373375
end
374376

375-
defp timestamp_attributes(true = _explicit_timestamp?, timestamp, submillis) do
377+
defp timestamp_attributes(true = _explicit_timestamp?, timestamp) do
378+
{timestamp, submillis} =
379+
Astarte.AppEngine.API.DateTime.split_submillis(timestamp)
380+
376381
%{
377382
value_timestamp: timestamp,
378383
reception_timestamp: timestamp,
379384
reception_timestamp_submillis: submillis
380385
}
381386
end
382387

383-
defp timestamp_attributes(_nil_or_false_explicit_timestamp?, timestamp, submillis) do
388+
defp timestamp_attributes(_nil_or_false_explicit_timestamp?, timestamp) do
389+
{timestamp, submillis} =
390+
Astarte.AppEngine.API.DateTime.split_submillis(timestamp)
391+
384392
%{reception_timestamp: timestamp, reception_timestamp_submillis: submillis}
385393
end
386394

@@ -1099,29 +1107,4 @@ defmodule Astarte.AppEngine.API.Device.Queries do
10991107
{:error, :database_error}
11001108
end
11011109
end
1102-
1103-
defp split_ms_and_submillis(timestamp_micro) do
1104-
timestamp_ms = div(timestamp_micro, 1000)
1105-
timestamp_submillis = rem(timestamp_micro, 1000)
1106-
1107-
{timestamp_ms, timestamp_submillis}
1108-
end
1109-
1110-
defp split_datetime_to_ms_and_submillis(datetime) do
1111-
datetime_ms = datetime |> DateTime.truncate(:millisecond) |> pad_usec()
1112-
1113-
{usec, _} = datetime.microsecond
1114-
submillis = usec |> rem(1000)
1115-
1116-
{datetime_ms, submillis}
1117-
end
1118-
1119-
defp pad_usec(nil), do: nil
1120-
1121-
defp pad_usec(timestamp) do
1122-
case timestamp.microsecond do
1123-
{_, 6} -> timestamp
1124-
{usec, _} -> %{timestamp | microsecond: {usec, 6}}
1125-
end
1126-
end
11271110
end

apps/astarte_appengine_api/lib/astarte_appengine_api/devices/device.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
defmodule Astarte.AppEngine.API.Devices.Device do
2020
use TypedEctoSchema
21+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
2122
alias Astarte.AppEngine.API.UUID
2223

2324
@primary_key {:device_id, UUID, autogenerate: false}
@@ -39,16 +40,16 @@ defmodule Astarte.AppEngine.API.Devices.Device do
3940
types: [:string, :integer],
4041
value: :integer
4142

42-
field :first_credentials_request, :utc_datetime_usec
43-
field :first_registration, :utc_datetime_usec
43+
field :first_credentials_request, DateTimeMs
44+
field :first_registration, DateTimeMs
4445
field :groups, Exandra.Map, key: :string, value: UUID
4546
field :inhibit_credentials_request, :boolean
4647
field :introspection, Exandra.Map, key: :string, value: :integer
4748
field :introspection_minor, Exandra.Map, key: :string, value: :integer
48-
field :last_connection, :utc_datetime_usec
49+
field :last_connection, DateTimeMs
4950
field :last_credentials_request_ip, Exandra.Inet
5051

51-
field :last_disconnection, :utc_datetime_usec
52+
field :last_disconnection, DateTimeMs
5253
field :last_seen_ip, Exandra.Inet
5354

5455
field :old_introspection,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.AppEngine.API.Realms.IndividualDatastream do
20+
use TypedEctoSchema
21+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
22+
alias Astarte.AppEngine.API.UUID
23+
24+
@primary_key false
25+
typed_schema "individual_datastreams" do
26+
field :reception, DateTimeMs, virtual: true
27+
field :device_id, UUID, primary_key: true
28+
field :interface_id, UUID, primary_key: true
29+
field :endpoint_id, UUID, primary_key: true
30+
field :path, :string, primary_key: true
31+
field :value_timestamp, DateTimeMs, primary_key: true
32+
field :reception_timestamp, DateTimeMs, primary_key: true
33+
field :reception_timestamp_submillis, :integer, primary_key: true
34+
field :binaryblob_value, :binary
35+
field :binaryblobarray_value, {:array, :binary}
36+
field :boolean_value, :boolean
37+
field :booleanarray_value, {:array, :boolean}
38+
field :datetime_value, DateTimeMs
39+
field :datetimearray_value, {:array, DateTimeMs}
40+
field :double_value, :float
41+
field :doublearray_value, {:array, :float}
42+
field :integer_value, :integer
43+
field :integerarray_value, {:array, :integer}
44+
field :longinteger_value, :integer
45+
field :longintegerarray_value, {:array, :integer}
46+
field :string_value, :string
47+
field :stringarray_value, {:array, :string}
48+
end
49+
50+
def prepare_for_db(%{reception: nil} = individual_datastream), do: individual_datastream
51+
52+
def prepare_for_db(individual_datastream) do
53+
{reception_ms, submillis} = DateTimeMs.split_submillis(individual_datastream.reception)
54+
55+
%{
56+
individual_datastream
57+
| reception_timestamp: reception_ms,
58+
reception_timestamp_submillis: submillis
59+
}
60+
end
61+
end

apps/astarte_appengine_api/lib/astarte_appengine_api/realms/individual_property.ex

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,43 @@
1818

1919
defmodule Astarte.AppEngine.API.Realms.IndividualProperty do
2020
use TypedEctoSchema
21+
alias Astarte.AppEngine.API.DateTime, as: DateTimeMs
2122
alias Astarte.AppEngine.API.UUID
2223

2324
@primary_key false
2425
typed_schema "individual_properties" do
25-
field :reception, :utc_datetime_usec, virtual: true
26+
field :reception, DateTimeMs, virtual: true
2627
field :device_id, UUID, primary_key: true
2728
field :interface_id, UUID, primary_key: true
2829
field :endpoint_id, UUID, primary_key: true
2930
field :path, :string, primary_key: true
30-
field :reception_timestamp, :utc_datetime_usec
31+
field :reception_timestamp, DateTimeMs
3132
field :reception_timestamp_submillis, :integer
3233
field :double_value, :float
3334
field :integer_value, :integer
3435
field :boolean_value, :boolean
3536
field :longinteger_value, :integer
3637
field :string_value, :string
3738
field :binaryblob_value, :binary
38-
field :datetime_value, :utc_datetime_usec
39+
field :datetime_value, DateTimeMs
3940
field :doublearray_value, {:array, :float}
4041
field :integerarray_value, {:array, :integer}
4142
field :booleanarray_value, {:array, :boolean}
4243
field :longintegerarray_value, {:array, :integer}
4344
field :stringarray_value, {:array, :string}
4445
field :binaryblobarray_value, {:array, :binary}
45-
field :datetimearray_value, {:array, :utc_datetime_usec}
46+
field :datetimearray_value, {:array, DateTimeMs}
4647
end
4748

48-
def reception(individual_property) do
49-
nanos =
50-
individual_property.reception_timestamp_submillis
51-
|> Kernel.||(0)
52-
|> Kernel.*(100)
49+
def prepare_for_db(%{reception: nil} = individual_property), do: individual_property
5350

54-
individual_property.reception_timestamp
55-
|> DateTime.add(nanos, :nanosecond)
51+
def prepare_for_db(individual_property) do
52+
{reception_ms, submillis} = DateTimeMs.split_submillis(individual_property.reception)
53+
54+
%{
55+
individual_property
56+
| reception_timestamp: reception_ms,
57+
reception_timestamp_submillis: submillis
58+
}
5659
end
5760
end

0 commit comments

Comments
 (0)