diff --git a/Gemfile b/Gemfile index 332ea2e9..f98083c7 100644 --- a/Gemfile +++ b/Gemfile @@ -79,7 +79,7 @@ gem 'good_job', '~> 4.0' gem 'rotp' gem 'grpc', '~> 1.67' -gem 'tucana', '0.0.47' +gem 'tucana', '0.0.0', path: '../tucana/build/ruby' gem 'code0-identities', '~> 0.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index 00c70f95..06a912da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,9 @@ +PATH + remote: ../tucana/build/ruby + specs: + tucana (0.0.0) + grpc (~> 1.64) + GEM remote: https://rubygems.org/ specs: @@ -376,8 +382,6 @@ GEM thor (1.4.0) timeout (0.4.4) tsort (0.2.0) - tucana (0.0.47) - grpc (~> 1.64) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.6.0) @@ -430,7 +434,7 @@ DEPENDENCIES simplecov (~> 0.22.0) simplecov-cobertura (~> 3.0) test-prof (~> 1.0) - tucana (= 0.0.47) + tucana (= 0.0.0)! tzinfo-data RUBY VERSION diff --git a/app/graphql/types/runtime_status_configuration_endpoint_type.rb b/app/graphql/types/runtime_status_configuration_endpoint_type.rb new file mode 100644 index 00000000..06fb1ed0 --- /dev/null +++ b/app/graphql/types/runtime_status_configuration_endpoint_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class RuntimeStatusConfigurationEndpointType < Types::BaseObject + description 'Detailed information about a runtime status' + + field :endpoint, String, null: false, description: 'The endpoint URL of the runtime' + + id_field ::RuntimeStatusConfiguration + timestamps + end +end diff --git a/app/graphql/types/runtime_status_configuration_type.rb b/app/graphql/types/runtime_status_configuration_type.rb new file mode 100644 index 00000000..52fa7966 --- /dev/null +++ b/app/graphql/types/runtime_status_configuration_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + class RuntimeStatusConfigurationType < Types::BaseUnion + description 'Detailed configuration about a runtime status, either: endpoint, ...' + + possible_types Types::RuntimeStatusConfigurationEndpointType + + def self.resolve_type(object, _context) + return Types::RuntimeStatusConfigurationEndpointType if object.endpoint.present? + + raise "Unknown RuntimeStatusInformation type: #{object.class.name}" + end + end +end diff --git a/app/graphql/types/runtime_status_enum.rb b/app/graphql/types/runtime_status_enum.rb new file mode 100644 index 00000000..6e552436 --- /dev/null +++ b/app/graphql/types/runtime_status_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class RuntimeStatusEnum < BaseEnum + description 'Represent all available aquila statuses' + + value :CONNECTED, 'No problem with the connection to aquila', value: :connected + value :DISCONNECTED, 'The runtime is disconnected, cause unknown', value: :disconnected + end +end diff --git a/app/graphql/types/runtime_status_status_enum.rb b/app/graphql/types/runtime_status_status_enum.rb new file mode 100644 index 00000000..927465a4 --- /dev/null +++ b/app/graphql/types/runtime_status_status_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class RuntimeStatusStatusEnum < Types::BaseEnum + description 'The enum status of the detailed status' + + value :NOT_RESPONDING, 'The runtime is not responding to heartbeats', value: 'NOT_RESPONDING' + value :NOT_READY, 'The runtime is not ready to compute stuff', value: 'NOT_READY' + value :RUNNING, 'The runtime is running and healthy', value: 'RUNNING' + value :STOPPED, 'The runtime has been stopped', value: 'STOPPED' + end +end diff --git a/app/graphql/types/runtime_status_type.rb b/app/graphql/types/runtime_status_type.rb index 1c514a27..052f328f 100644 --- a/app/graphql/types/runtime_status_type.rb +++ b/app/graphql/types/runtime_status_type.rb @@ -1,10 +1,25 @@ # frozen_string_literal: true module Types - class RuntimeStatusType < BaseEnum - description 'Represent all available types of statuses of a runtime' + class RuntimeStatusType < Types::BaseObject + description 'A runtime status information entry' - value :CONNECTED, 'No problem with connection, everything works as expected', value: 'connected' - value :DISCONNECTED, 'The runtime is disconnected, cause unknown', value: 'disconnected' + field :configurations, Types::RuntimeStatusConfigurationType.connection_type, + null: false, + description: 'The detailed configuration entries for this runtime status (only for adapters)', + method: :runtime_status_configurations + field :features, [String], null: false, description: 'The set of features supported by the runtime' + field :identifier, String, null: false, description: 'The unique identifier for this runtime status' + field :last_heartbeat, Types::TimeType, null: true, + description: 'The timestamp of the last heartbeat received from the runtime' + field :status, Types::RuntimeStatusStatusEnum, + null: false, + description: 'The current status of the runtime (e.g. running, stopped)' + field :type, Types::RuntimeStatusTypeEnum, + null: false, + description: 'The type of runtime status information (e.g. adapter, execution)', method: :status_type + + id_field RuntimeStatus + timestamps end end diff --git a/app/graphql/types/runtime_status_type_enum.rb b/app/graphql/types/runtime_status_type_enum.rb new file mode 100644 index 00000000..fb0ee092 --- /dev/null +++ b/app/graphql/types/runtime_status_type_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class RuntimeStatusTypeEnum < Types::BaseEnum + description 'The type of runtime status' + + value :ADAPTER, 'Indicates that the runtime status is related to an adapter.', value: 'adapter' + value :EXECUTION, 'Indicates that the runtime status is related to an execution.', value: 'execution' + end +end diff --git a/app/graphql/types/runtime_type.rb b/app/graphql/types/runtime_type.rb index 22026e1b..80094ef9 100644 --- a/app/graphql/types/runtime_type.rb +++ b/app/graphql/types/runtime_type.rb @@ -16,10 +16,16 @@ class RuntimeType < Types::BaseObject field :runtime_function_definitions, Types::RuntimeFunctionDefinitionType.connection_type, null: false, description: 'Functions of the runtime' - field :status, Types::RuntimeStatusType, null: false, description: 'The status of the runtime' field :token, String, null: true, description: 'Token belonging to the runtime, only present on creation' + field :status, Types::RuntimeStatusStatusEnum, + null: false, + description: 'Wheater the last heartbeat was recent enough to consider the runtime as connected' + field :statuses, Types::RuntimeStatusType.connection_type, null: false, + description: 'Statuses of the runtime', + method: :runtime_statuses + expose_abilities %i[ delete_runtime update_runtime @@ -29,6 +35,17 @@ class RuntimeType < Types::BaseObject id_field Runtime timestamps + # If the last heartbeat was within the last 10 minutes, consider the runtime as 'running' + def status + last_heartbeat = object.last_heartbeat + + if last_heartbeat && last_heartbeat >= 10.minutes.ago + :connected + else + :disconnected + end + end + def token object.token if object.token_previously_changed? end diff --git a/app/grpc/flow_handler.rb b/app/grpc/flow_handler.rb index 868b57cd..8235b3f0 100644 --- a/app/grpc/flow_handler.rb +++ b/app/grpc/flow_handler.rb @@ -8,6 +8,9 @@ class FlowHandler < Tucana::Sagittarius::FlowService::Service grpc_stream :update def self.update_runtime(runtime) + runtime.last_heartbeat = Time.zone.now + runtime.save! + flows = [] runtime.project_assignments.compatible.each do |assignment| assignment.namespace_project.flows.each do |flow| @@ -25,22 +28,12 @@ def self.update_runtime(runtime) ) end - def self.update_started(runtime_id) - runtime = Runtime.find(runtime_id) - runtime.connected! - runtime.save - + def self.update_started(_runtime_id) logger.info(message: 'Runtime connected', runtime_id: runtime.id) update_runtime(runtime) end - def self.update_died(runtime_id) - runtime = Runtime.find(runtime_id) - runtime.disconnected! - runtime.save - end - def self.encoders = { update: ->(grpc_object) { Tucana::Sagittarius::FlowResponse.encode(grpc_object) } } def self.decoders = { update: ->(string) { Tucana::Sagittarius::FlowResponse.decode(string) } } diff --git a/app/grpc/runtime_status_handler.rb b/app/grpc/runtime_status_handler.rb new file mode 100644 index 00000000..f688e6c7 --- /dev/null +++ b/app/grpc/runtime_status_handler.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class RuntimeStatusHandler < Tucana::Sagittarius::RuntimeStatusService::Service + include Code0::ZeroTrack::Loggable + include GrpcHandler + + def update(request, _call) + current_runtime = Runtime.find(Code0::ZeroTrack::Context.current[:runtime][:id]) + + response = Runtimes::Grpc::RuntimeStatusUpdateService.new( + runtime: current_runtime, + status_info: request.send(request.status) + ).execute + + logger.debug("RuntimeFunctionHandler#update response: #{response.inspect}") + + Tucana::Sagittarius::RuntimeStatusUpdateResponse.new(success: response.success?) + end +end diff --git a/app/models/runtime.rb b/app/models/runtime.rb index 74697f39..3a35b701 100644 --- a/app/models/runtime.rb +++ b/app/models/runtime.rb @@ -3,21 +3,11 @@ class Runtime < ApplicationRecord include TokenAttr - STATUS_TYPES = { - disconnected: 0, - connected: 1, - }.with_indifferent_access - belongs_to :namespace, optional: true token_attr :token, prefix: 's_rt_', length: 48 - enum :status, STATUS_TYPES, default: :disconnected - - validates :status, presence: true, - inclusion: { - in: STATUS_TYPES.keys.map(&:to_s), - } + has_many :runtime_statuses, inverse_of: :runtime has_many :project_assignments, class_name: 'NamespaceProjectRuntimeAssignment', inverse_of: :runtime has_many :projects, class_name: 'NamespaceProject', through: :project_assignments, source: :namespace_project, diff --git a/app/models/runtime_status.rb b/app/models/runtime_status.rb new file mode 100644 index 00000000..862f5c3b --- /dev/null +++ b/app/models/runtime_status.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class RuntimeStatus < ApplicationRecord + belongs_to :runtime, inverse_of: :runtime_statuses + has_many :runtime_status_configurations, inverse_of: :runtime_status + + STATUS_TYPES = { + not_responding: 0, + not_ready: 1, + running: 2, + stopped: 3, + }.with_indifferent_access + + enum :status, STATUS_TYPES, default: :stopped + + STATUS_TYPE_TYPES = { + adapter: 0, + execution: 1, + }.with_indifferent_access + + enum :status_type, STATUS_TYPE_TYPES + + validates :identifier, presence: true, + allow_blank: false, + uniqueness: { case_sensitive: false, scope: :runtime_id } + + validate :runtime_status_informations_only_for_adapter + + private + + def runtime_status_informations_only_for_adapter + return if adapter? + return if runtime_status_configurations.empty? + + errors.add(:runtime_status_informations, :only_allowed_for_adapters) + end +end diff --git a/app/models/runtime_status_configuration.rb b/app/models/runtime_status_configuration.rb new file mode 100644 index 00000000..a1bbb17f --- /dev/null +++ b/app/models/runtime_status_configuration.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class RuntimeStatusConfiguration < ApplicationRecord + belongs_to :runtime_status, inverse_of: :runtime_status_configurations + + validates :endpoint, presence: true, + allow_blank: false +end diff --git a/app/services/error_code.rb b/app/services/error_code.rb index e51c5abd..9f8772a0 100644 --- a/app/services/error_code.rb +++ b/app/services/error_code.rb @@ -87,6 +87,8 @@ def self.error_codes function_value_not_found: { description: 'The id for the function value node does not exist' }, invalid_node_parameter: { description: 'The node parameter is invalid' }, invalid_node_function: { description: 'The node function is invalid' }, + invalid_runtime_status: { description: 'The runtime status is invalid because of active model errors' }, + invalid_runtime_status_configuration: { description: 'The runtime status configuration is invalid because of active model errors' }, primary_level_not_found: { description: '', deprecation_reason: 'Outdated concept' }, secondary_level_not_found: { description: '', deprecation_reason: 'Outdated concept' }, diff --git a/app/services/runtimes/grpc/runtime_status_update_service.rb b/app/services/runtimes/grpc/runtime_status_update_service.rb new file mode 100644 index 00000000..36bd6073 --- /dev/null +++ b/app/services/runtimes/grpc/runtime_status_update_service.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Runtimes + module Grpc + class RuntimeStatusUpdateService + include Sagittarius::Database::Transactional + include Code0::ZeroTrack::Loggable + + attr_reader :runtime, :status_info + + def initialize(runtime:, status_info:) + @runtime = runtime + @status_info = status_info + end + + def execute + transactional do |t| + runtime.last_heartbeat = Time.zone.now + + unless runtime.save + t.rollback_and_return ServiceResponse.error( + message: 'Failed to update runtime heartbeat', + error_code: :invalid_runtime, + details: runtime.errors + ) + end + + db_status = RuntimeStatus.find_or_initialize_by(runtime: runtime, + identifier: status_info.identifier) + + db_status.last_heartbeat = Time.zone.at(status_info.timestamp.to_i) + db_status.status_type = if status_info.is_a?(Tucana::Shared::AdapterRuntimeStatus) + :adapter + else + :execution + end + db_status.features = status_info.features.to_a + + db_status.status = status_info.status.downcase + + db_configs = db_status.runtime_status_configurations.first(status_info.configurations.size) + + status_info.configurations.each_with_index do |config, index| + db_configs[index] ||= db_status.runtime_status_configurations.build + + db_configs[index].endpoint = config.endpoint + + next if db_configs[index].save + + t.rollback_and_return! ServiceResponse.error( + message: 'Failed to save runtime status configuration', + error_code: :invalid_runtime_status_configuration, + details: db_configs.errors + ) + end + + unless db_status.save + t.rollback_and_return! ServiceResponse.error( + message: 'Failed to save runtime status', + error_code: :invalid_runtime_status, + details: db_status.errors + ) + end + + return ServiceResponse.success( + message: 'Updated runtime status' + ) + end + end + end + end +end diff --git a/db/migrate/20251216161818_add_detailed_runtime_status.rb b/db/migrate/20251216161818_add_detailed_runtime_status.rb new file mode 100644 index 00000000..8e938b14 --- /dev/null +++ b/db/migrate/20251216161818_add_detailed_runtime_status.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class AddDetailedRuntimeStatus < Code0::ZeroTrack::Database::Migration[1.0] + def change + remove_column :runtimes, :status, :integer, null: false, default: 0 + add_column :runtimes, :last_heartbeat, :datetime_with_timezone + + create_table :runtime_statuses do |t| + t.references :runtime, null: false, foreign_key: { to_table: :runtimes, on_delete: :cascade } + t.integer :status, null: false, default: 0 + t.integer :status_type, null: false, default: 0 + t.datetime_with_timezone :last_heartbeat + t.text :identifier, null: false + t.text :features, array: true, default: [], null: false + + t.timestamps_with_timezone + end + + create_table :runtime_status_configurations do |t| + t.references :runtime_status, null: false, + foreign_key: { to_table: :runtime_statuses, on_delete: :cascade } + t.text :endpoint, null: false + + t.timestamps_with_timezone + end + end +end diff --git a/db/schema_migrations/20251216161818 b/db/schema_migrations/20251216161818 new file mode 100644 index 00000000..a9a3687f --- /dev/null +++ b/db/schema_migrations/20251216161818 @@ -0,0 +1 @@ +1747b0add7b91129d147ba739eca9403635c401e7e9d865d498d0c9d5aad0c19 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 421fa7d5..45d2ee12 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -724,6 +724,44 @@ CREATE SEQUENCE runtime_parameter_definitions_id_seq ALTER SEQUENCE runtime_parameter_definitions_id_seq OWNED BY runtime_parameter_definitions.id; +CREATE TABLE runtime_status_configurations ( + id bigint NOT NULL, + runtime_status_id bigint NOT NULL, + endpoint text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE runtime_status_configurations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE runtime_status_configurations_id_seq OWNED BY runtime_status_configurations.id; + +CREATE TABLE runtime_statuses ( + id bigint NOT NULL, + runtime_id bigint NOT NULL, + status integer DEFAULT 0 NOT NULL, + status_type integer DEFAULT 0 NOT NULL, + last_heartbeat timestamp with time zone, + identifier text NOT NULL, + features text[] DEFAULT '{}'::text[] NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE runtime_statuses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE runtime_statuses_id_seq OWNED BY runtime_statuses.id; + CREATE TABLE runtimes ( id bigint NOT NULL, name text NOT NULL, @@ -732,7 +770,7 @@ CREATE TABLE runtimes ( namespace_id bigint, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - status integer DEFAULT 0 NOT NULL, + last_heartbeat timestamp with time zone, CONSTRAINT check_090cd49d30 CHECK ((char_length(name) <= 50)), CONSTRAINT check_f3c2ba8db3 CHECK ((char_length(description) <= 500)) ); @@ -901,6 +939,10 @@ ALTER TABLE ONLY runtime_function_definitions ALTER COLUMN id SET DEFAULT nextva ALTER TABLE ONLY runtime_parameter_definitions ALTER COLUMN id SET DEFAULT nextval('runtime_parameter_definitions_id_seq'::regclass); +ALTER TABLE ONLY runtime_status_configurations ALTER COLUMN id SET DEFAULT nextval('runtime_status_configurations_id_seq'::regclass); + +ALTER TABLE ONLY runtime_statuses ALTER COLUMN id SET DEFAULT nextval('runtime_statuses_id_seq'::regclass); + ALTER TABLE ONLY runtimes ALTER COLUMN id SET DEFAULT nextval('runtimes_id_seq'::regclass); ALTER TABLE ONLY translations ALTER COLUMN id SET DEFAULT nextval('translations_id_seq'::regclass); @@ -1031,6 +1073,12 @@ ALTER TABLE ONLY runtime_function_definitions ALTER TABLE ONLY runtime_parameter_definitions ADD CONSTRAINT runtime_parameter_definitions_pkey PRIMARY KEY (id); +ALTER TABLE ONLY runtime_status_configurations + ADD CONSTRAINT runtime_status_configurations_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY runtime_statuses + ADD CONSTRAINT runtime_statuses_pkey PRIMARY KEY (id); + ALTER TABLE ONLY runtimes ADD CONSTRAINT runtimes_pkey PRIMARY KEY (id); @@ -1213,6 +1261,10 @@ CREATE INDEX index_runtime_function_definitions_on_return_type_id ON runtime_fun CREATE INDEX index_runtime_parameter_definitions_on_data_type_id ON runtime_parameter_definitions USING btree (data_type_id); +CREATE INDEX index_runtime_status_configurations_on_runtime_status_id ON runtime_status_configurations USING btree (runtime_status_id); + +CREATE INDEX index_runtime_statuses_on_runtime_id ON runtime_statuses USING btree (runtime_id); + CREATE INDEX index_runtimes_on_namespace_id ON runtimes USING btree (namespace_id); CREATE UNIQUE INDEX index_runtimes_on_token ON runtimes USING btree (token); @@ -1271,6 +1323,9 @@ ALTER TABLE ONLY generic_types ALTER TABLE ONLY namespace_licenses ADD CONSTRAINT fk_rails_38f693332d FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY runtime_statuses + ADD CONSTRAINT fk_rails_3af887feb9 FOREIGN KEY (runtime_id) REFERENCES runtimes(id) ON DELETE CASCADE; + ALTER TABLE ONLY parameter_definitions ADD CONSTRAINT fk_rails_3b02763f84 FOREIGN KEY (runtime_parameter_definition_id) REFERENCES runtime_parameter_definitions(id) ON DELETE CASCADE; @@ -1391,6 +1446,9 @@ ALTER TABLE ONLY generic_mappers ALTER TABLE ONLY parameter_definitions ADD CONSTRAINT fk_rails_ca0a397b6f FOREIGN KEY (data_type_id) REFERENCES data_type_identifiers(id) ON DELETE RESTRICT; +ALTER TABLE ONLY runtime_status_configurations + ADD CONSTRAINT fk_rails_d3eeb850ed FOREIGN KEY (runtime_status_id) REFERENCES runtime_statuses(id) ON DELETE CASCADE; + ALTER TABLE ONLY namespace_projects ADD CONSTRAINT fk_rails_d4f50e2f00 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/docs/graphql/enum/errorcodeenum.md b/docs/graphql/enum/errorcodeenum.md index e8a0e6a5..f71c0584 100644 --- a/docs/graphql/enum/errorcodeenum.md +++ b/docs/graphql/enum/errorcodeenum.md @@ -47,6 +47,7 @@ Represents the available error responses | `INVALID_RUNTIME_FUNCTION_ID` | The runtime function ID is invalid | | `INVALID_RUNTIME_PARAMETER_DEFINITION` | The runtime parameter definition is invalid | | `INVALID_RUNTIME_PARAMETER_ID` | The runtime parameter ID is invalid | +| `INVALID_RUNTIME_STATUS` | The runtime status is invalid because of active model errors | | `INVALID_SETTING` | Invalid setting provided | | `INVALID_TOTP_SECRET` | The TOTP secret is invalid or cannot be verified | | `INVALID_USER` | The user is invalid because of active model errors | diff --git a/docs/graphql/enum/runtimestatusstatus.md b/docs/graphql/enum/runtimestatusstatus.md new file mode 100644 index 00000000..0b95fe30 --- /dev/null +++ b/docs/graphql/enum/runtimestatusstatus.md @@ -0,0 +1,12 @@ +--- +title: RuntimeStatusStatus +--- + +The enum status of the detailed status + +| Value | Description | +|-------|-------------| +| `NOT_READY` | The runtime is not ready to compute stuff | +| `NOT_RESPONDING` | The runtime is not responding to heartbeats | +| `RUNNING` | The runtime is running and healthy | +| `STOPPED` | The runtime has been stopped | diff --git a/docs/graphql/enum/runtimestatustype.md b/docs/graphql/enum/runtimestatustype.md index 08e3952a..01431bfb 100644 --- a/docs/graphql/enum/runtimestatustype.md +++ b/docs/graphql/enum/runtimestatustype.md @@ -2,9 +2,9 @@ title: RuntimeStatusType --- -Represent all available types of statuses of a runtime +The type of runtime status | Value | Description | |-------|-------------| -| `CONNECTED` | No problem with connection, everything works as expected | -| `DISCONNECTED` | The runtime is disconnected, cause unknown | +| `ADAPTER` | Indicates that the runtime status is related to an adapter. | +| `EXECUTION` | Indicates that the runtime status is related to an execution. | diff --git a/docs/graphql/object/runtime.md b/docs/graphql/object/runtime.md index d9dee5e0..8f3d9fee 100644 --- a/docs/graphql/object/runtime.md +++ b/docs/graphql/object/runtime.md @@ -17,7 +17,8 @@ Represents a runtime | `namespace` | [`Namespace`](../object/namespace.md) | The parent namespace for the runtime | | `projects` | [`NamespaceProjectConnection!`](../object/namespaceprojectconnection.md) | Projects associated with the runtime | | `runtimeFunctionDefinitions` | [`RuntimeFunctionDefinitionConnection!`](../object/runtimefunctiondefinitionconnection.md) | Functions of the runtime | -| `status` | [`RuntimeStatusType!`](../enum/runtimestatustype.md) | The status of the runtime | +| `status` | [`RuntimeStatusStatus!`](../enum/runtimestatusstatus.md) | Wheater the last heartbeat was recent enough to consider the runtime as connected | +| `statuses` | [`RuntimeStatusConnection!`](../object/runtimestatusconnection.md) | Statuses of the runtime | | `token` | [`String`](../scalar/string.md) | Token belonging to the runtime, only present on creation | | `updatedAt` | [`Time!`](../scalar/time.md) | Time when this Runtime was last updated | | `userAbilities` | [`RuntimeUserAbilities!`](../object/runtimeuserabilities.md) | Abilities for the current user on this Runtime | diff --git a/docs/graphql/object/runtimestatus.md b/docs/graphql/object/runtimestatus.md new file mode 100644 index 00000000..0c7bbd5e --- /dev/null +++ b/docs/graphql/object/runtimestatus.md @@ -0,0 +1,20 @@ +--- +title: RuntimeStatus +--- + +A runtime status information entry + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `configurations` | [`RuntimeStatusConfigurationConnection!`](../object/runtimestatusconfigurationconnection.md) | The detailed configuration entries for this runtime status (only for adapters) | +| `createdAt` | [`Time!`](../scalar/time.md) | Time when this RuntimeStatus was created | +| `featureSet` | [`[String!]!`](../scalar/string.md) | The set of features supported by the runtime | +| `id` | [`RuntimeStatusID!`](../scalar/runtimestatusid.md) | Global ID of this RuntimeStatus | +| `identifier` | [`String!`](../scalar/string.md) | The unique identifier for this runtime status | +| `lastHeartbeat` | [`Time`](../scalar/time.md) | The timestamp of the last heartbeat received from the runtime | +| `status` | [`RuntimeStatusStatus!`](../enum/runtimestatusstatus.md) | The current status of the runtime (e.g. running, stopped) | +| `type` | [`RuntimeStatusType!`](../enum/runtimestatustype.md) | The type of runtime status information (e.g. adapter, execution) | +| `updatedAt` | [`Time!`](../scalar/time.md) | Time when this RuntimeStatus was last updated | + diff --git a/docs/graphql/object/runtimestatusconfigurationconnection.md b/docs/graphql/object/runtimestatusconfigurationconnection.md new file mode 100644 index 00000000..afc0ee12 --- /dev/null +++ b/docs/graphql/object/runtimestatusconfigurationconnection.md @@ -0,0 +1,15 @@ +--- +title: RuntimeStatusConfigurationConnection +--- + +The connection type for RuntimeStatusConfiguration. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `count` | [`Int!`](../scalar/int.md) | Total count of collection. | +| `edges` | [`[RuntimeStatusConfigurationEdge]`](../object/runtimestatusconfigurationedge.md) | A list of edges. | +| `nodes` | [`[RuntimeStatusConfiguration]`](../union/runtimestatusconfiguration.md) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](../object/pageinfo.md) | Information to aid in pagination. | + diff --git a/docs/graphql/object/runtimestatusconfigurationedge.md b/docs/graphql/object/runtimestatusconfigurationedge.md new file mode 100644 index 00000000..46cc5104 --- /dev/null +++ b/docs/graphql/object/runtimestatusconfigurationedge.md @@ -0,0 +1,13 @@ +--- +title: RuntimeStatusConfigurationEdge +--- + +An edge in a connection. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `cursor` | [`String!`](../scalar/string.md) | A cursor for use in pagination. | +| `node` | [`RuntimeStatusConfiguration`](../union/runtimestatusconfiguration.md) | The item at the end of the edge. | + diff --git a/docs/graphql/object/runtimestatusconfigurationendpoint.md b/docs/graphql/object/runtimestatusconfigurationendpoint.md new file mode 100644 index 00000000..e11ae71a --- /dev/null +++ b/docs/graphql/object/runtimestatusconfigurationendpoint.md @@ -0,0 +1,15 @@ +--- +title: RuntimeStatusConfigurationEndpoint +--- + +Detailed information about a runtime status + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `createdAt` | [`Time!`](../scalar/time.md) | Time when this RuntimeStatusConfigurationEndpoint was created | +| `endpoint` | [`String!`](../scalar/string.md) | The endpoint URL of the runtime | +| `id` | [`RuntimeStatusConfigurationID!`](../scalar/runtimestatusconfigurationid.md) | Global ID of this RuntimeStatusConfigurationEndpoint | +| `updatedAt` | [`Time!`](../scalar/time.md) | Time when this RuntimeStatusConfigurationEndpoint was last updated | + diff --git a/docs/graphql/object/runtimestatusconnection.md b/docs/graphql/object/runtimestatusconnection.md new file mode 100644 index 00000000..8b9d2e0b --- /dev/null +++ b/docs/graphql/object/runtimestatusconnection.md @@ -0,0 +1,15 @@ +--- +title: RuntimeStatusConnection +--- + +The connection type for RuntimeStatus. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `count` | [`Int!`](../scalar/int.md) | Total count of collection. | +| `edges` | [`[RuntimeStatusEdge]`](../object/runtimestatusedge.md) | A list of edges. | +| `nodes` | [`[RuntimeStatus]`](../object/runtimestatus.md) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](../object/pageinfo.md) | Information to aid in pagination. | + diff --git a/docs/graphql/object/runtimestatusedge.md b/docs/graphql/object/runtimestatusedge.md new file mode 100644 index 00000000..cc784d95 --- /dev/null +++ b/docs/graphql/object/runtimestatusedge.md @@ -0,0 +1,13 @@ +--- +title: RuntimeStatusEdge +--- + +An edge in a connection. + +## Fields without arguments + +| Name | Type | Description | +|------|------|-------------| +| `cursor` | [`String!`](../scalar/string.md) | A cursor for use in pagination. | +| `node` | [`RuntimeStatus`](../object/runtimestatus.md) | The item at the end of the edge. | + diff --git a/docs/graphql/scalar/runtimestatusconfigurationid.md b/docs/graphql/scalar/runtimestatusconfigurationid.md new file mode 100644 index 00000000..9b34bd34 --- /dev/null +++ b/docs/graphql/scalar/runtimestatusconfigurationid.md @@ -0,0 +1,5 @@ +--- +title: RuntimeStatusConfigurationID +--- + +A unique identifier for all RuntimeStatusConfiguration entities of the application diff --git a/docs/graphql/scalar/runtimestatusid.md b/docs/graphql/scalar/runtimestatusid.md new file mode 100644 index 00000000..1ae3b817 --- /dev/null +++ b/docs/graphql/scalar/runtimestatusid.md @@ -0,0 +1,5 @@ +--- +title: RuntimeStatusID +--- + +A unique identifier for all RuntimeStatus entities of the application diff --git a/docs/graphql/union/runtimestatusconfiguration.md b/docs/graphql/union/runtimestatusconfiguration.md new file mode 100644 index 00000000..145bff9b --- /dev/null +++ b/docs/graphql/union/runtimestatusconfiguration.md @@ -0,0 +1,9 @@ +--- +title: RuntimeStatusConfiguration +--- + +Detailed configuration about a runtime status, either: endpoint, ... + +## Possible types + +- [`RuntimeStatusConfigurationEndpoint`](../object/runtimestatusconfigurationendpoint.md) diff --git a/spec/factories/runtime_status.rb b/spec/factories/runtime_status.rb new file mode 100644 index 00000000..970751b1 --- /dev/null +++ b/spec/factories/runtime_status.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :runtime_status do + status { :stopped } + last_heartbeat { Time.zone.today } + status_type { :adapter } + identifier { SecureRandom.uuid } + features { [] } + runtime + end +end diff --git a/spec/factories/runtime_status_configuration.rb b/spec/factories/runtime_status_configuration.rb new file mode 100644 index 00000000..5e9f4f97 --- /dev/null +++ b/spec/factories/runtime_status_configuration.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :runtime_status_configuration do + runtime_status + endpoint { 'http://example.com' } + end +end diff --git a/spec/graphql/types/runtime_status_type_spec.rb b/spec/graphql/types/runtime_status_type_spec.rb new file mode 100644 index 00000000..5bc67d0b --- /dev/null +++ b/spec/graphql/types/runtime_status_type_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SagittariusSchema.types['RuntimeStatus'] do + let(:fields) do + %w[ + id + status + configurations + featureSet + lastHeartbeat + type + identifier + createdAt + updatedAt + ] + end + + it { expect(described_class.graphql_name).to eq('RuntimeStatus') } + it { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/spec/graphql/types/runtime_type_spec.rb b/spec/graphql/types/runtime_type_spec.rb index a59b6d67..3b6e03a3 100644 --- a/spec/graphql/types/runtime_type_spec.rb +++ b/spec/graphql/types/runtime_type_spec.rb @@ -13,6 +13,7 @@ runtimeFunctionDefinitions description projects + statuses status token userAbilities diff --git a/spec/models/runtime_spec.rb b/spec/models/runtime_spec.rb index 796bfc6e..7f3cfce1 100644 --- a/spec/models/runtime_spec.rb +++ b/spec/models/runtime_spec.rb @@ -21,6 +21,8 @@ is_expected.to have_many(:projects).class_name('NamespaceProject').through(:project_assignments) .inverse_of(:runtimes) } + + it { is_expected.to have_many(:runtime_statuses).inverse_of(:runtime) } end describe 'validations' do diff --git a/spec/models/runtime_status_spec.rb b/spec/models/runtime_status_spec.rb new file mode 100644 index 00000000..c1938f61 --- /dev/null +++ b/spec/models/runtime_status_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RuntimeStatus do + subject(:runtime_status) { create(:runtime_status) } + + describe 'associations' do + it { is_expected.to have_many(:runtime_status_configurations).inverse_of(:runtime_status) } + it { is_expected.to belong_to(:runtime).inverse_of(:runtime_statuses) } + end + + describe 'validations' do + context 'when status information exist' do + before do + create(:runtime_status_configuration, runtime_status: runtime_status) + end + + context 'when type is :adapter' do + before { runtime_status.status_type = :adapter } + + it 'is valid' do + expect(runtime_status).to be_valid + end + end + + context 'when type is :execution' do + before { runtime_status.status_type = :execution } + + it 'is invalid' do + expect(runtime_status).not_to be_valid + end + end + end + end +end diff --git a/spec/requests/grpc/sagittarius/runtime_status_update_service_spec.rb b/spec/requests/grpc/sagittarius/runtime_status_update_service_spec.rb new file mode 100644 index 00000000..e7ba093d --- /dev/null +++ b/spec/requests/grpc/sagittarius/runtime_status_update_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'sagittarius.RuntimeStatusServiceRuntimeFunctionDefinitionService', :need_grpc_server do + include GrpcHelpers + + let(:stub) { create_stub Tucana::Sagittarius::RuntimeStatusService } + + describe 'Update' do + let(:runtime) { create(:runtime) } + let(:to_update_status) do + Tucana::Shared::AdapterRuntimeStatus.new( + status: Tucana::Shared::RuntimeStatus::RUNNING, + timestamp: Time.now.to_i.to_s, + identifier: 'adapter_status_1', + features: ['http'], + configurations: [ + Tucana::Shared::AdapterConfiguration.new( + endpoint: 'http://localhost:3000' + ) + ] + ) + end + + let(:message) do + Tucana::Sagittarius::RuntimeStatusUpdateRequest.new(adapter_runtime_status: to_update_status) + end + + it 'creates a correct functions' do + expect(stub.update(message, authorization(runtime)).success).to be(true) + db_status = RuntimeStatus.last + expect(db_status.runtime).to eq(runtime) + expect(db_status.identifier).to eq('adapter_status_1') + expect(db_status.status_type).to eq('adapter') + expect(db_status.status).to eq('running') + expect(db_status.features).to eq(['http']) + expect(db_status.runtime_status_configurations.count).to eq(1) + expect(db_status.runtime_status_configurations.first.endpoint).to eq('http://localhost:3000') + end + + context 'when old configuration exists before' do + before do + create(:runtime_status_configuration, endpoint: 'http://old-endpoint.com', + runtime_status: create(:runtime_status, + runtime: runtime, + identifier: 'adapter_status_1')) + end + + it 'updates the existing configuration' do + expect(stub.update(message, authorization(runtime)).success).to be(true) + expect(RuntimeStatusConfiguration.count).to eq(1) + config = RuntimeStatusConfiguration.last + expect(config.endpoint).to eq('http://localhost:3000') + end + end + end +end