% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. % % SPDX-License-Identifier: AGPL-3.0-only % % Author: Philipp Maier / sysmocom - s.f.m.c. GmbH -module(mnesia_db_rest). -include_lib("stdlib/include/qlc.hrl"). -include("mnesia_db_rest.hrl"). -include("mnesia_db_work.hrl"). % REST functions, to be called by the REST server (from outside via cowboy) -export([list/1, lookup/2, create/3, delete/2]). % debugging -export([dump/0]). % trigger recurring events (called automatically by timer from this module) -export([timer_cleanup/0]). % transaction functions (to be called from a transaction) -export([trans_set_status/4]). % helper function (to be called from a transaction) to set the status of an item in the rest table. This function % is intended to be called from mnesia_db_work to report back the status of a specific work item. trans_set_status(ResourceId, Status, Outcome, Debuginfo) -> Q = qlc:q([X || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), Rows = qlc:e(Q), Timestamp = os:system_time(seconds), case Rows of [Row | []] -> mnesia:write(Row#rest{ status = Status, timestamp = Timestamp, outcome = Outcome, debuginfo = Debuginfo }), ok; [] -> error; _ -> error end. % Create REST resource (order) create(Facility, EidValue, Order) -> % Ensure that an euicc entry is present in the database for the given EidValue. In case no entry exists yet, a new % entry is created with default parameters from sys.config, which is is sufficient in many usecases. From the % technical perspective, this is just a convenience feature, which frees the REST API user from having to create an % euicc entry manually before performming the first operation. ok = mnesia_db_euicc:create_if_not_exist(EidValue), % Continue with the normal creation of the REST resource. ResourceId = uuid:uuid_to_string(uuid:get_v4_urandom()), Timestamp = os:system_time(seconds), Row = #rest{ resourceId = ResourceId, facility = Facility, eidValue = EidValue, order = Order, status = new, timestamp = Timestamp, outcome = [], debuginfo = none }, Trans = fun() -> Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]), Present = qlc:e(Q), case Present of [] -> mnesia:write(Row); _ -> error end end, {atomic, Result} = mnesia:transaction(Trans), {Result, ResourceId}. % Lookup REST resource (order) lookup(ResourceId, Facility) -> Trans = fun() -> Q = qlc:q([ { X#rest.status, X#rest.timestamp, X#rest.eidValue, X#rest.order, X#rest.outcome, X#rest.debuginfo } || X <- mnesia:table(rest), X#rest.resourceId == ResourceId, X#rest.facility == Facility ]), qlc:e(Q) end, {atomic, Result} = mnesia:transaction(Trans), case Result of [{Status, Timestamp, EidValue, Order, Outcome, Debuginfo}] -> {Status, Timestamp, EidValue, Order, Outcome, Debuginfo}; [] -> none; _ -> error end. % Delete REST resource (order) delete(ResourceId, Facility) -> Trans = fun() -> QRest = qlc:q([ X#rest.resourceId || X <- mnesia:table(rest), X#rest.resourceId == ResourceId, X#rest.facility == Facility ]), RestPresent = qlc:e(QRest), case RestPresent of [] -> none; _ -> OidRest = {rest, ResourceId}, ok = mnesia:delete(OidRest), % There may be an orphaned work item now, which we must also remove. This will also kill % the order in case it is currently in progress. QWork = qlc:q([ X#work.pid || X <- mnesia:table(work), X#work.resourceId == ResourceId ]), WorkPresent = qlc:e(QWork), case WorkPresent of [] -> ok; [Pid] -> OidWork = {work, Pid}, mnesia:delete(OidWork); _ -> error end end end, {atomic, Result} = mnesia:transaction(Trans), Result. % List the resource identifiers of currently present REST resources list(Facility) -> Trans = fun() -> Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.facility == Facility]), qlc:e(Q) end, {atomic, Result} = mnesia:transaction(Trans), Result. mark_stuck(Timeout) -> % There may be corner cases where the order gets stuck because the other entity suddenly stops responding. In % this case the order will stall. When it remains stalled for too long (minutes), we should remove all related % items from the work table and put an appropriate outcome (error code) into the rest table. TimestampNow = os:system_time(seconds), % Remove Resource from work table and set an appropriate status in the rest table HandleResource = fun(ResourceId) -> % find the pid of the work item that is stuck and then delete it Q = qlc:q([X#work.pid || X <- mnesia:table(work), X#work.resourceId == ResourceId]), WorkPresent = qlc:e(Q), case WorkPresent of [Pid] -> Oid = {work, Pid}, ok = mnesia:delete(Oid); _ -> ok end, % set status in the rest table trans_set_status(ResourceId, done, [{[{procedureError, stuckOrder}]}], none) end, % Find all rest resources that stall in status "work" and older than the specified timeout value Trans = fun() -> Q = qlc:q([ X#rest.resourceId || X <- mnesia:table(rest), X#rest.status == work, TimestampNow - X#rest.timestamp > Timeout ]), Rows = qlc:e(Q), case Rows of [] -> ok; Rows -> [HandleResource(Row) || Row <- Rows], ok end end, {atomic, Result} = mnesia:transaction(Trans), Result. mark_noshow(Timeout) -> % There may be corner cases where the order is never processed because the related IPAd/eUICC never shows up to % fetch the related eUICC package. If we see an order staying in status "new" for too long (days, weeks), we should % mark it as "done" and put an appropriate outcome (error code) into the rest table. TimestampNow = os:system_time(seconds), % Remove Resource from work table and set an appropriate status in the rest table HandleResource = fun(ResourceId) -> trans_set_status(ResourceId, done, [{[{procedureError, noshowOrder}]}], none) end, % Find all rest resources that stall in status "work" and older than the specified timeout value Trans = fun() -> Q = qlc:q([ X#rest.resourceId || X <- mnesia:table(rest), X#rest.status == new, TimestampNow - X#rest.timestamp > Timeout ]), Rows = qlc:e(Q), case Rows of [] -> ok; Rows -> [HandleResource(Row) || Row <- Rows], ok end end, {atomic, Result} = mnesia:transaction(Trans), Result. delete_expired(Timeout) -> % There may be cases where orders stay unmaintained for too long. When an order stays in status "done" for too long % (hours, days), than this may mean that the REST API user lost interest. In this case the related items should be % removed from the rest table after a reasonable timeout. TimestampNow = os:system_time(seconds), % Remove Resource from the rest table. Since an abandonned order won't have a coresponding work item in the work % table we do not have to worry about creating an orphaned work item. HandleResource = fun(ResourceId) -> Oid = {rest, ResourceId}, ok = mnesia:delete(Oid) end, % Find all rest resources that linger in the rest table for a long time and are not in the status "new" Trans = fun() -> Q = qlc:q([ X#rest.resourceId || X <- mnesia:table(rest), X#rest.status == done, TimestampNow - X#rest.timestamp > Timeout ]), Rows = qlc:e(Q), case Rows of [] -> ok; Rows -> [HandleResource(Row) || Row <- Rows], ok end end, {atomic, Result} = mnesia:transaction(Trans), Result. % Run a cleanup cycle the database timer_cleanup() -> {ok, RestTimeoutStuck} = application:get_env(onomondo_eim, rest_timeout_stuck), {ok, RestTimeoutNoshow} = application:get_env(onomondo_eim, rest_timeout_noshow), {ok, RestTimeoutExpired} = application:get_env(onomondo_eim, rest_timeout_expired), RcStalled = mark_stuck(RestTimeoutStuck), RcNoshow = mark_noshow(RestTimeoutNoshow), RcExpired = delete_expired(RestTimeoutExpired), case {RcStalled, RcNoshow, RcExpired} of {ok, ok, ok} -> ok; _ -> logger:error("Cleanup: database error~n"), error end, % Next cleanup in 10 secs. {ok, _} = timer:apply_after(10000, mnesia_db_rest, timer_cleanup, []), ok. % Dump all currently pending rest items (for debugging, to be called from console) dump() -> Trans = fun() -> Q = qlc:q([X || X <- mnesia:table(rest)]), qlc:e(Q) end, {atomic, Result} = mnesia:transaction(Trans), Result.