diff --git a/lib/trento/application/projectors/database_projector.ex b/lib/trento/application/projectors/database_projector.ex index 944e5d0ca6..d74233b7a5 100644 --- a/lib/trento/application/projectors/database_projector.ex +++ b/lib/trento/application/projectors/database_projector.ex @@ -22,7 +22,8 @@ defmodule Trento.DatabaseProjector do DatabaseInstanceHealthChanged, DatabaseInstanceRegistered, DatabaseInstanceSystemReplicationChanged, - DatabaseRegistered + DatabaseRegistered, + DatabaseRestored } alias Trento.Repo @@ -160,6 +161,24 @@ defmodule Trento.DatabaseProjector do end ) + project( + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: health + }, + fn multi -> + db = Repo.get!(DatabaseReadModel, sap_system_id) + + changeset = + DatabaseReadModel.changeset( + db, + %{deregistered_at: nil, health: health} + ) + + Ecto.Multi.update(multi, :database, changeset) + end + ) + project( %DatabaseInstanceDeregistered{ instance_number: instance_number, @@ -279,6 +298,21 @@ defmodule Trento.DatabaseProjector do ) end + @impl true + def after_update( + %DatabaseRestored{sap_system_id: sap_system_id}, + _, + _ + ) do + database = Repo.get!(DatabaseReadModel, sap_system_id) + + TrentoWeb.Endpoint.broadcast( + @databases_topic, + "database_registered", + SapSystemView.render("database_registered.json", database: database) + ) + end + @impl true def after_update( %DatabaseDeregistered{ diff --git a/lib/trento/application/projectors/sap_system_projector.ex b/lib/trento/application/projectors/sap_system_projector.ex index c85a9f3914..86d7d73683 100644 --- a/lib/trento/application/projectors/sap_system_projector.ex +++ b/lib/trento/application/projectors/sap_system_projector.ex @@ -15,6 +15,7 @@ defmodule Trento.SapSystemProjector do SapSystemDeregistered, SapSystemHealthChanged, SapSystemRegistered, + SapSystemRestored, SapSystemUpdated } @@ -134,6 +135,28 @@ defmodule Trento.SapSystemProjector do end ) + project( + %SapSystemRestored{ + sap_system_id: sap_system_id, + tenant: tenant, + db_host: db_host, + health: health + }, + fn multi -> + sap_system = Repo.get!(SapSystemReadModel, sap_system_id) + + changeset = + SapSystemReadModel.changeset(sap_system, %{ + tenant: tenant, + db_host: db_host, + health: health, + deregistered_at: nil + }) + + Ecto.Multi.update(multi, :sap_system, changeset) + end + ) + project( %ApplicationInstanceDeregistered{ instance_number: instance_number, @@ -170,6 +193,7 @@ defmodule Trento.SapSystemProjector do @sap_systems_topic "monitoring:sap_systems" @impl true + @spec after_update(any, any, any) :: :ok | {:error, any} def after_update( %SapSystemRegistered{}, _, @@ -258,6 +282,21 @@ defmodule Trento.SapSystemProjector do ) end + @impl true + def after_update( + %SapSystemRestored{sap_system_id: sap_system_id}, + _, + _ + ) do + sap_system = Repo.get!(SapSystemReadModel, sap_system_id) + + TrentoWeb.Endpoint.broadcast( + @sap_systems_topic, + "sap_system_registered", + SapSystemView.render("sap_system_registered.json", sap_system: sap_system) + ) + end + @impl true def after_update( %ApplicationInstanceDeregistered{ diff --git a/lib/trento/domain/sap_system/events/database_restored.ex b/lib/trento/domain/sap_system/events/database_restored.ex new file mode 100644 index 0000000000..e486ab69ef --- /dev/null +++ b/lib/trento/domain/sap_system/events/database_restored.ex @@ -0,0 +1,14 @@ +defmodule Trento.Domain.Events.DatabaseRestored do + @moduledoc """ + This event is emitted when a database is restored. + """ + + use Trento.Event + + require Trento.Domain.Enums.Health, as: Health + + defevent do + field :sap_system_id, Ecto.UUID + field :health, Ecto.Enum, values: Health.values() + end +end diff --git a/lib/trento/domain/sap_system/events/sap_system_restored.ex b/lib/trento/domain/sap_system/events/sap_system_restored.ex new file mode 100644 index 0000000000..cae01e3908 --- /dev/null +++ b/lib/trento/domain/sap_system/events/sap_system_restored.ex @@ -0,0 +1,16 @@ +defmodule Trento.Domain.Events.SapSystemRestored do + @moduledoc """ + This event is emitted when a sap system is restored. + """ + + use Trento.Event + + require Trento.Domain.Enums.Health, as: Health + + defevent do + field :sap_system_id, Ecto.UUID + field :tenant, :string + field :db_host, :string + field :health, Ecto.Enum, values: Health.values() + end +end diff --git a/lib/trento/domain/sap_system/sap_system.ex b/lib/trento/domain/sap_system/sap_system.ex index dcc81cf94f..7944dda8cc 100644 --- a/lib/trento/domain/sap_system/sap_system.ex +++ b/lib/trento/domain/sap_system/sap_system.ex @@ -77,9 +77,11 @@ defmodule Trento.Domain.SapSystem do DatabaseInstanceRegistered, DatabaseInstanceSystemReplicationChanged, DatabaseRegistered, + DatabaseRestored, SapSystemDeregistered, SapSystemHealthChanged, SapSystemRegistered, + SapSystemRestored, SapSystemRolledUp, SapSystemRollUpRequested, SapSystemTombstoned, @@ -161,6 +163,60 @@ defmodule Trento.Domain.SapSystem do ] end + # Database restore + def execute( + %SapSystem{database: %Database{deregistered_at: deregistered_at}}, + %RegisterDatabaseInstance{ + system_replication: "Secondary" + } + ) + when not is_nil(deregistered_at), + do: {:error, :sap_system_not_registered} + + # When a deregistered database is present, we add the new database instance + # and restore the database, the conditions are the same as registration + def execute( + %SapSystem{database: %Database{deregistered_at: deregistered_at}}, + %RegisterDatabaseInstance{ + sap_system_id: sap_system_id, + sid: sid, + tenant: tenant, + host_id: host_id, + instance_number: instance_number, + instance_hostname: instance_hostname, + features: features, + http_port: http_port, + https_port: https_port, + start_priority: start_priority, + system_replication: system_replication, + system_replication_status: system_replication_status, + health: health + } + ) + when not is_nil(deregistered_at) do + [ + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: health + }, + %DatabaseInstanceRegistered{ + sap_system_id: sap_system_id, + sid: sid, + tenant: tenant, + instance_number: instance_number, + instance_hostname: instance_hostname, + features: features, + http_port: http_port, + https_port: https_port, + start_priority: start_priority, + host_id: host_id, + system_replication: system_replication, + system_replication_status: system_replication_status, + health: health + } + ] + end + # When a RegisterDatabaseInstance command is received by an existing SAP System aggregate, # the SAP System aggregate registers the Database instance if it is not already registered # and updates the health when needed. @@ -185,6 +241,26 @@ defmodule Trento.Domain.SapSystem do |> Multi.execute(&maybe_emit_sap_system_health_changed_event/1) end + # Restore sap system + # Same registration rules + def execute( + %SapSystem{deregistered_at: deregistered_at} = sap_system, + %RegisterApplicationInstance{} = instance + ) + when not is_nil(deregistered_at) do + sap_system + |> Multi.new() + |> Multi.execute(fn sap_system -> + maybe_emit_application_instance_registered_or_moved_event( + sap_system, + instance + ) + end) + |> Multi.execute(fn sap_system -> + maybe_emit_sap_system_restored_event(sap_system, instance) + end) + end + # SAP system not registered, application already present # If the instance is not one of MESSAGESERVER or ABAP we discard. # Otherwise if the instance we want register together with already present instances @@ -615,6 +691,22 @@ defmodule Trento.Domain.SapSystem do } end + def apply( + %SapSystem{database: database} = sap_system, + %DatabaseRestored{ + health: health + } + ) do + %SapSystem{ + sap_system + | database: %Database{ + database + | health: health, + deregistered_at: nil + } + } + end + def apply( %SapSystem{} = sap_system, %SapSystemDeregistered{ @@ -627,6 +719,16 @@ defmodule Trento.Domain.SapSystem do } end + def apply(%SapSystem{} = sap_system, %SapSystemRestored{ + health: health + }) do + %SapSystem{ + sap_system + | health: health, + deregistered_at: nil + } + end + def apply(%SapSystem{} = sap_system, %SapSystemTombstoned{}), do: sap_system defp maybe_emit_database_instance_registered_event( @@ -829,6 +931,25 @@ defmodule Trento.Domain.SapSystem do end end + defp maybe_emit_sap_system_restored_event( + %SapSystem{application: %Application{instances: instances}}, + %RegisterApplicationInstance{ + sap_system_id: sap_system_id, + tenant: tenant, + db_host: db_host, + health: health + } + ) do + if instances_have_abap?(instances) and instances_have_messageserver?(instances) do + %SapSystemRestored{ + db_host: db_host, + health: health, + sap_system_id: sap_system_id, + tenant: tenant + } + end + end + defp maybe_emit_sap_system_registered_or_updated_event( %SapSystem{sid: nil, application: %Application{instances: instances}}, %RegisterApplicationInstance{ diff --git a/test/support/factory.ex b/test/support/factory.ex index 9691fe54d1..185aef2204 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -21,6 +21,7 @@ defmodule Trento.Factory do } alias Trento.Domain.Events.{ + ApplicationInstanceDeregistered, ApplicationInstanceRegistered, ClusterDeregistered, ClusterRegistered, @@ -29,6 +30,7 @@ defmodule Trento.Factory do DatabaseInstanceDeregistered, DatabaseInstanceRegistered, DatabaseRegistered, + DatabaseRestored, HostAddedToCluster, HostDetailsUpdated, HostRegistered, @@ -267,6 +269,13 @@ defmodule Trento.Factory do }) end + def database_restored_event_factory do + DatabaseRestored.new!(%{ + sap_system_id: Faker.UUID.v4(), + health: Health.passing() + }) + end + def deregister_database_instance_command_factory do DeregisterDatabaseInstance.new!(%{ sap_system_id: Faker.UUID.v4(), @@ -298,6 +307,15 @@ defmodule Trento.Factory do } end + def application_instance_deregistered_event_factory do + ApplicationInstanceDeregistered.new!(%{ + sap_system_id: Faker.UUID.v4(), + deregistered_at: DateTime.utc_now(), + instance_number: "00", + host_id: Faker.UUID.v4() + }) + end + def deregister_application_instance_command_factory do DeregisterApplicationInstance.new!(%{ sap_system_id: Faker.UUID.v4(), diff --git a/test/trento/application/projectors/database_projector_test.exs b/test/trento/application/projectors/database_projector_test.exs index dc994d5ecb..9f76bb4daa 100644 --- a/test/trento/application/projectors/database_projector_test.exs +++ b/test/trento/application/projectors/database_projector_test.exs @@ -18,7 +18,8 @@ defmodule Trento.DatabaseProjectorTest do DatabaseHealthChanged, DatabaseInstanceDeregistered, DatabaseInstanceHealthChanged, - DatabaseInstanceSystemReplicationChanged + DatabaseInstanceSystemReplicationChanged, + DatabaseRestored } alias Trento.ProjectorTestHelper @@ -305,4 +306,32 @@ defmodule Trento.DatabaseProjectorTest do }, 1000 end + + test "should restore a deregistered database when DatabaseRestored is received" do + insert(:database, + id: sap_system_id = Faker.UUID.v4(), + sid: "NWD", + deregistered_at: DateTime.utc_now(), + health: :critical + ) + + event = %DatabaseRestored{ + sap_system_id: sap_system_id, + health: :passing + } + + ProjectorTestHelper.project(DatabaseProjector, event, "database_projector") + + projection = Repo.get(DatabaseReadModel, sap_system_id) + assert nil == projection.deregistered_at + assert :passing == projection.health + + assert_broadcast "database_registered", + %{ + health: :passing, + id: ^sap_system_id, + sid: "NWD" + }, + 1000 + end end diff --git a/test/trento/application/projectors/sap_system_projector_test.exs b/test/trento/application/projectors/sap_system_projector_test.exs index 2543aa274b..0cc3f066a5 100644 --- a/test/trento/application/projectors/sap_system_projector_test.exs +++ b/test/trento/application/projectors/sap_system_projector_test.exs @@ -20,6 +20,7 @@ defmodule Trento.SapSystemProjectorTest do ApplicationInstanceHealthChanged, SapSystemDeregistered, SapSystemHealthChanged, + SapSystemRestored, SapSystemUpdated } @@ -188,6 +189,37 @@ defmodule Trento.SapSystemProjectorTest do assert deregistered_at == projection.deregistered_at end + test "should restore a SAP system when SapSystemRestored is received" do + %{tenant: tenant, id: sap_system_id, sid: sid} = + insert(:sap_system, deregistered_at: DateTime.utc_now()) + + new_db_host = Faker.Internet.ip_v4_address() + new_health = :passing + + event = %SapSystemRestored{ + sap_system_id: sap_system_id, + tenant: tenant, + db_host: new_db_host, + health: new_health + } + + ProjectorTestHelper.project(SapSystemProjector, event, "sap_system_projector") + + projection = Repo.get(SapSystemReadModel, sap_system_id) + + assert_broadcast "sap_system_registered", + %{ + db_host: ^new_db_host, + health: ^new_health, + id: ^sap_system_id, + sid: ^sid, + tenant: ^tenant + }, + 1000 + + assert nil == projection.deregistered_at + end + test "should remove an application instance from the read model after a deregistration" do deregistered_at = DateTime.utc_now() diff --git a/test/trento/domain/sap_system/sap_system_test.exs b/test/trento/domain/sap_system/sap_system_test.exs index 23c5218e27..83025c1b44 100644 --- a/test/trento/domain/sap_system/sap_system_test.exs +++ b/test/trento/domain/sap_system/sap_system_test.exs @@ -25,9 +25,11 @@ defmodule Trento.SapSystemTest do DatabaseInstanceRegistered, DatabaseInstanceSystemReplicationChanged, DatabaseRegistered, + DatabaseRestored, SapSystemDeregistered, SapSystemHealthChanged, SapSystemRegistered, + SapSystemRestored, SapSystemRolledUp, SapSystemRollUpRequested, SapSystemTombstoned, @@ -1817,6 +1819,907 @@ defmodule Trento.SapSystemTest do end describe "deregistration" do + test "should not restore a deregistered database when the registering database instance has Secondary role" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ) + ] + + command = + build(:register_database_instance_command, + system_replication: "Secondary", + sid: db_sid, + sap_system_id: sap_system_id + ) + + assert_error( + initial_events, + command, + {:error, :sap_system_not_registered} + ) + end + + test "should restore a deregistered database when the registering database instance has system replication disabled, with database instance leftovers" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ) + ] + + %{features: features, instance_number: instance_number, health: health} = + command = + build(:register_database_instance_command, + system_replication: nil, + sid: db_sid, + sap_system_id: sap_system_id + ) + + assert_events_and_state( + initial_events, + command, + [ + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: command.health + }, + %DatabaseInstanceRegistered{ + sap_system_id: sap_system_id, + sid: db_sid, + tenant: command.tenant, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + host_id: command.host_id, + system_replication: command.system_replication, + system_replication_status: command.system_replication_status, + health: command.health + } + ], + fn sap_system -> + assert %SapSystem{ + deregistered_at: ^deregistered_at, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid, + instances: [ + %Instance{ + sid: ^db_sid, + instance_number: ^instance_number, + features: ^features, + health: ^health + }, + %Instance{} + ] + } + } = sap_system + end + ) + end + + test "should restore a deregistered database when the registering database instance has system replication disabled, without database instance leftovers" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ) + ] + + %{features: features, instance_number: instance_number, health: health} = + command = + build(:register_database_instance_command, + system_replication: nil, + sid: db_sid, + sap_system_id: sap_system_id + ) + + assert_events_and_state( + initial_events, + command, + [ + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: command.health + }, + %DatabaseInstanceRegistered{ + sap_system_id: sap_system_id, + sid: db_sid, + tenant: command.tenant, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + host_id: command.host_id, + system_replication: command.system_replication, + system_replication_status: command.system_replication_status, + health: command.health + } + ], + fn sap_system -> + assert Kernel.length(sap_system.database.instances) == 1 + + assert %SapSystem{ + deregistered_at: ^deregistered_at, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid, + instances: [ + %Instance{ + sid: ^db_sid, + instance_number: ^instance_number, + features: ^features, + health: ^health, + system_replication: nil + } + ] + } + } = sap_system + end + ) + end + + test "should restore a deregistered database when the registering database instance is a primary, without database instance leftovers" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ) + ] + + %{features: features, instance_number: instance_number, health: health} = + command = + build(:register_database_instance_command, + system_replication: "Primary", + sid: db_sid, + sap_system_id: sap_system_id + ) + + assert_events_and_state( + initial_events, + command, + [ + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: command.health + }, + %DatabaseInstanceRegistered{ + sap_system_id: sap_system_id, + sid: db_sid, + tenant: command.tenant, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + host_id: command.host_id, + system_replication: command.system_replication, + system_replication_status: command.system_replication_status, + health: command.health + } + ], + fn sap_system -> + assert Kernel.length(sap_system.database.instances) == 1 + + assert %SapSystem{ + deregistered_at: ^deregistered_at, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid, + instances: [ + %Instance{ + sid: ^db_sid, + instance_number: ^instance_number, + features: ^features, + health: ^health, + system_replication: "Primary" + } + ] + } + } = sap_system + end + ) + end + + test "should restore a deregistered database when the registering database instance is a primary, with database instance leftovers" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ) + ] + + %{features: features, instance_number: instance_number, health: health} = + command = + build(:register_database_instance_command, + system_replication: "Primary", + sid: db_sid, + sap_system_id: sap_system_id + ) + + assert_events_and_state( + initial_events, + command, + [ + %DatabaseRestored{ + sap_system_id: sap_system_id, + health: command.health + }, + %DatabaseInstanceRegistered{ + sap_system_id: sap_system_id, + sid: db_sid, + tenant: command.tenant, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + host_id: command.host_id, + system_replication: command.system_replication, + system_replication_status: command.system_replication_status, + health: command.health + } + ], + fn sap_system -> + assert Kernel.length(sap_system.database.instances) == 2 + + assert %SapSystem{ + deregistered_at: ^deregistered_at, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid, + instances: [ + %Instance{ + sid: ^db_sid, + instance_number: ^instance_number, + features: ^features, + health: ^health, + system_replication: "Primary" + }, + %Instance{} + ] + } + } = sap_system + end + ) + end + + test "should not restore a sap system when no abap/messageserver instances are present" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:application_instance_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at, + instance_number: message_server_instance_number, + host_id: message_server_host_id + ), + build(:database_instance_registered_event, + system_replication: "Primary", + sid: db_sid, + sap_system_id: sap_system_id + ), + build(:database_restored_event, + sap_system_id: sap_system_id + ) + ] + + command = + build( + :register_application_instance_command, + sap_system_id: sap_system_id, + sid: application_sid, + db_host: primary_database_host_id, + features: "IGS" + ) + + assert_events_and_state( + initial_events, + command, + [ + %ApplicationInstanceRegistered{ + sap_system_id: sap_system_id, + sid: application_sid, + host_id: command.host_id, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + health: command.health + } + ], + fn sap_system -> + assert %SapSystem{ + deregistered_at: ^deregistered_at, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid + } + } = sap_system + end + ) + end + + test "should restore a sap system when abap/messageserver instances are present" do + sap_system_id = UUID.uuid4() + + primary_database_host_id = UUID.uuid4() + secondary_database_host_id = UUID.uuid4() + + deregistered_at = DateTime.utc_now() + + db_instance_number_1 = "00" + db_instance_number_2 = "01" + + db_sid = fake_sid() + application_sid = fake_sid() + + message_server_host_id = UUID.uuid4() + message_server_instance_number = "00" + abap_host_id = UUID.uuid4() + abap_instance_number = "01" + + initial_events = [ + build( + :database_registered_event, + sap_system_id: sap_system_id, + sid: db_sid + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + sid: db_sid, + instance_number: db_instance_number_1, + system_replication: "Primary" + ), + build( + :database_instance_registered_event, + sap_system_id: sap_system_id, + host_id: secondary_database_host_id, + instance_number: db_instance_number_2, + system_replication: "Secondary", + sid: db_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "MESSAGESERVER|ENQUE", + host_id: message_server_host_id, + instance_number: message_server_instance_number, + sid: application_sid + ), + build( + :application_instance_registered_event, + sap_system_id: sap_system_id, + features: "ABAP|GATEWAY|ICMAN|IGS", + host_id: abap_host_id, + instance_number: abap_instance_number, + sid: application_sid + ), + build( + :sap_system_registered_event, + sap_system_id: sap_system_id, + sid: application_sid + ), + build(:database_instance_deregistered_event, + sap_system_id: sap_system_id, + host_id: primary_database_host_id, + instance_number: db_instance_number_1, + deregistered_at: deregistered_at + ), + build(:database_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:sap_system_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at + ), + build(:application_instance_deregistered_event, + sap_system_id: sap_system_id, + deregistered_at: deregistered_at, + instance_number: message_server_instance_number, + host_id: message_server_host_id + ), + build(:database_instance_registered_event, + system_replication: "Primary", + sid: db_sid, + sap_system_id: sap_system_id + ), + build(:database_restored_event, + sap_system_id: sap_system_id + ) + ] + + command = + build( + :register_application_instance_command, + sap_system_id: sap_system_id, + sid: application_sid, + db_host: primary_database_host_id, + features: "MESSAGESERVER" + ) + + assert_events_and_state( + initial_events, + command, + [ + %ApplicationInstanceRegistered{ + sap_system_id: sap_system_id, + sid: application_sid, + host_id: command.host_id, + instance_number: command.instance_number, + instance_hostname: command.instance_hostname, + features: command.features, + http_port: command.http_port, + https_port: command.https_port, + start_priority: command.start_priority, + health: command.health + }, + %SapSystemRestored{ + sap_system_id: sap_system_id, + tenant: command.tenant, + db_host: command.db_host, + health: command.health + } + ], + fn sap_system -> + assert %SapSystem{ + deregistered_at: nil, + database: %Database{ + deregistered_at: nil, + sid: ^db_sid + } + } = sap_system + end + ) + end + test "should reject all the commands except for the registration/instance deregistration ones, when the SAP system is deregistered" do sap_system_id = UUID.uuid4()