Files
firezone/elixir/apps/domain/lib/domain/changes/hooks/resources.ex
Jamil cafe6554ff refactor(portal): reduce cache memory usage (#10058)
Napkin math shows that we can save substantial memory (~3x or more) on
the API nodes as connected clients/gateways grow if we just store the
fields we need in order to keep the client and gateway state maintained
in the channel pids.

To facilitate this, we create new `Cacheable` structs that represent
their `Domain` cousins, which use byte arrays for `id`s and strip out
unused fields.

Additionally, all business logic involved with maintaining these caches
is now contained within two modules: `Domain.Cache.Client` and
`Domain.Cache.Gateway`, and type specs have been added to aid in static
analysis and code documentation.

Comprehensive testing is now added not only for the cache modules, but
for their associated channel modules as well to ensure we handle
different kinds of edge cases gracefully.

The `Events` nomenclature was renamed to `Changes` to better name what
we are doing: Change-Data-Capture.

Lastly, the following related changes are included in this PR since they
were "in the way" so to speak of getting this done:

- We save the last received LSN in each channel and drop the `change`
with a warning if we receive it twice in a row, or we receive it out of
order
- The client/gateway version compatibility calculations have been moved
to `Domain.Resources` and `Domain.Gateways` and have been simplified to
make them easier to understand and maintain going forward.


Related: #10174 
Fixes: #9392 
Fixes: #9965
Fixes: #9501 
Fixes: #10227

---------

Signed-off-by: Jamil <jamilbk@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-22 21:52:29 +00:00

58 lines
2.0 KiB
Elixir

defmodule Domain.Changes.Hooks.Resources do
@behaviour Domain.Changes.Hooks
alias Domain.{Changes.Change, Flows, PubSub, Resources}
import Domain.SchemaHelpers
@impl true
def on_insert(lsn, data) do
resource = struct_from_params(Resources.Resource, data)
change = %Change{lsn: lsn, op: :insert, struct: resource}
PubSub.Account.broadcast(resource.account_id, change)
end
@impl true
# Soft-delete - process as delete
def on_update(lsn, %{"deleted_at" => nil} = old_data, %{"deleted_at" => deleted_at})
when not is_nil(deleted_at) do
on_delete(lsn, old_data)
end
# Regular update
def on_update(lsn, old_data, data) do
old_resource = struct_from_params(Resources.Resource, old_data)
resource = struct_from_params(Resources.Resource, data)
change = %Change{lsn: lsn, op: :update, old_struct: old_resource, struct: resource}
# Breaking updates
# This is a special case - we need to delete related flows because connectivity has changed
# Gateway _does_ handle resource filter changes so we don't need to delete flows
# for those changes - they're processed by the Gateway channel pid.
# The Gateway channel will process these flow deletions and re-authorize the flow.
# However, the gateway will also react to the resource update and send reject_access
# so that the Gateway's state is updated correctly, and the client can create a new flow.
if old_resource.ip_stack != resource.ip_stack or
old_resource.type != resource.type or
old_resource.address != resource.address do
Flows.delete_flows_for(resource)
end
PubSub.Account.broadcast(resource.account_id, change)
end
@impl true
def on_delete(lsn, old_data) do
resource = struct_from_params(Resources.Resource, old_data)
change = %Change{lsn: lsn, op: :delete, old_struct: resource}
# TODO: Hard delete
# This can be removed upon implementation of hard delete
Flows.delete_flows_for(resource)
PubSub.Account.broadcast(resource.account_id, change)
end
end