-
Notifications
You must be signed in to change notification settings - Fork 0
runtime usage service #960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
raphael-goetz
wants to merge
11
commits into
main
Choose a base branch
from
#779-runtime-usage-service
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
1fc9160
feat: created weekly runtime usage migration
raphael-goetz e929734
feat: wip runtime usage service
raphael-goetz 0a8c806
feat: switched weekly interval for to daily
raphael-goetz a6d3364
feat: added default value for usage
raphael-goetz 2bebe3d
feat: added runtime usage service
raphael-goetz dc6fdfe
feat: added runtime usage grpc handler
raphael-goetz aae60f1
feat: ipdated specs
raphael-goetz 0347a28
feat: exposed daily runtime usage to graphql interface
raphael-goetz 2bf9dfc
fix: correct identification for flow id
raphael-goetz fd1f680
feat: added indexes
raphael-goetz 7df5ad3
feat: used db increment
raphael-goetz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Types | ||
| class DailyRuntimeUsageType < Types::BaseObject | ||
| description 'Represents runtime usage for a flow on a specific day' | ||
|
|
||
| authorize :read_namespace | ||
| declarative_policy_subject(&:namespace) | ||
|
|
||
| field :day, Types::DateType, null: false, description: 'The day this usage was recorded for' | ||
| field :flow, Types::FlowType, null: true, description: 'The flow this usage was recorded for' | ||
| field :namespace, Types::NamespaceType, null: false, description: 'The namespace this usage belongs to' | ||
| field :usage, Float, null: false, description: 'The accumulated runtime usage for the day' | ||
|
|
||
| id_field DailyRuntimeUsage | ||
| timestamps | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Types | ||
| class DateType < BaseScalar | ||
| description <<~DESC | ||
| Date represented in ISO 8601. | ||
|
|
||
| For example: "2026-05-12". | ||
| DESC | ||
|
|
||
| def self.coerce_input(value, _ctx) | ||
| return if value.nil? | ||
|
|
||
| Date.iso8601(value) | ||
| rescue ArgumentError, TypeError => e | ||
| raise GraphQL::CoercionError, e.message | ||
| end | ||
|
|
||
| def self.coerce_result(value, _ctx) | ||
| value.iso8601 | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class RuntimeUsageHandler < Tucana::Sagittarius::RuntimeUsageService::Service | ||
| include Code0::ZeroTrack::Loggable | ||
| include GrpcHandler | ||
|
|
||
| def update(request, _call) | ||
| current_runtime = Runtime.find(Code0::ZeroTrack::Context.current[:runtime][:id]) | ||
| response = Runtimes::Grpc::RuntimeUsageUpdateService.new( | ||
| runtime: current_runtime, | ||
| usages: request.runtime_usage | ||
| ).execute | ||
|
|
||
| logger.debug("RuntimeUsageHandler#update response: #{response.inspect}") | ||
|
|
||
| Tucana::Sagittarius::RuntimeUsageResponse.new(success: response.success?) | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class DailyRuntimeUsage < ApplicationRecord | ||
| belongs_to :flow, optional: true, inverse_of: :daily_runtime_usages | ||
| belongs_to :namespace, inverse_of: :daily_runtime_usages | ||
|
|
||
| validates :day, presence: true | ||
| validates :usage, numericality: { greater_than_or_equal_to: 0 } | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
app/services/runtimes/grpc/runtime_usage_update_service.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Runtimes | ||
| module Grpc | ||
| class RuntimeUsageUpdateService | ||
| include Sagittarius::Database::Transactional | ||
| include Code0::ZeroTrack::Loggable | ||
|
|
||
| attr_reader :runtime, :usages | ||
|
|
||
| def initialize(runtime:, usages:) | ||
| @runtime = runtime | ||
| @usages = usages | ||
| end | ||
|
|
||
| def execute | ||
| transactional do |t| | ||
| updated_usages = [] | ||
|
|
||
| Array.wrap(usages).each do |usage| | ||
| result = update_usage(usage) | ||
| t.rollback_and_return! result if result.error? | ||
|
|
||
| updated_usages << result.payload | ||
| end | ||
|
|
||
| ServiceResponse.success(message: 'Updated runtime usage', payload: updated_usages) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def update_usage(usage) | ||
| flow = Flow.includes(project: :namespace).find_by(id: usage_attribute(usage, :flow_id)) | ||
| return ServiceResponse.error(message: 'Flow not found', error_code: :flow_not_found) if flow.nil? | ||
| return runtime_assignment_error(flow) unless runtime_assigned_to_flow?(flow) | ||
|
|
||
| day = usage_day(usage) | ||
| amount = usage_amount(usage) | ||
| return invalid_usage_error('Usage amount must be greater than zero') unless amount&.positive? | ||
|
|
||
| db_usage, created = find_or_create_usage(flow, day, amount) | ||
|
|
||
| return ServiceResponse.success(payload: db_usage) if created | ||
|
|
||
| # rubocop:disable Rails/SkipsModelValidations -- amount is validated above; this keeps the increment atomic in SQL. | ||
| DailyRuntimeUsage.update_counters(db_usage.id, usage: amount, touch: true) | ||
| # rubocop:enable Rails/SkipsModelValidations | ||
| ServiceResponse.success(payload: db_usage.reload) | ||
| rescue ActiveRecord::RecordInvalid => e | ||
| invalid_usage_error(e.record.errors) | ||
| rescue ArgumentError | ||
| invalid_usage_error('Usage interval must be a valid date') | ||
| end | ||
|
|
||
| def find_or_create_usage(flow, day, amount) | ||
| attributes = { | ||
| namespace: flow.project.namespace, | ||
| flow: flow, | ||
| day: day, | ||
| } | ||
|
|
||
| db_usage = DailyRuntimeUsage.find_by(attributes) | ||
| return [db_usage, false] if db_usage.present? | ||
|
|
||
| db_usage = nil | ||
| DailyRuntimeUsage.transaction(requires_new: true) do | ||
| db_usage = DailyRuntimeUsage.create!(attributes.merge(usage: amount)) | ||
| end | ||
|
|
||
| [db_usage, true] | ||
| rescue ActiveRecord::RecordNotUnique | ||
| retry | ||
| end | ||
|
|
||
| def usage_day(usage) | ||
| value = usage_attribute(usage, :day, :date, :interval) | ||
| return Time.zone.today if value.nil? | ||
|
|
||
| case value | ||
| when Date | ||
| value | ||
| when Time | ||
| value.to_date | ||
| when String | ||
| Date.iso8601(value) | ||
| else | ||
| Time.zone.at(value.seconds).to_date if value.respond_to?(:seconds) | ||
| end | ||
| end | ||
|
|
||
| def usage_amount(usage) | ||
| value = usage_attribute(usage, :duration, :usage, :amount, :count) | ||
| return if value.nil? | ||
|
|
||
| BigDecimal(value.to_s) | ||
| rescue ArgumentError | ||
| nil | ||
| end | ||
|
|
||
| def runtime_assigned_to_flow?(flow) | ||
| runtime.project_assignments.compatible.exists?(namespace_project: flow.project) | ||
| end | ||
|
|
||
| def runtime_assignment_error(flow) | ||
| assignment = runtime.project_assignments.find_by(namespace_project: flow.project) | ||
| if assignment.nil? | ||
| return ServiceResponse.error( | ||
| message: 'Runtime not assigned to flow project', | ||
| error_code: :runtime_not_assigned | ||
| ) | ||
| end | ||
|
|
||
| ServiceResponse.error(message: 'Runtime not compatible with flow project', error_code: :runtime_not_compatible) | ||
| end | ||
|
|
||
| def usage_attribute(usage, *keys) | ||
| keys.each do |key| | ||
| return usage.public_send(key) if usage.respond_to?(key) | ||
| return usage[key] if usage.respond_to?(:key?) && usage.key?(key) | ||
| return usage[key.to_s] if usage.respond_to?(:key?) && usage.key?(key.to_s) | ||
| end | ||
|
|
||
| nil | ||
| end | ||
|
|
||
| def invalid_usage_error(details) | ||
| ServiceResponse.error( | ||
| message: 'Failed to update runtime usage', | ||
| error_code: :invalid_runtime_usage, | ||
| details: details | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class CreateDailyRuntimeUsage < Code0::ZeroTrack::Database::Migration[1.0] | ||
| def change | ||
| create_table :daily_runtime_usages do |t| | ||
| t.references :flow, null: true, foreign_key: { to_table: :flows, on_delete: :nullify } | ||
| t.references :namespace, null: false, foreign_key: { to_table: :namespaces, on_delete: :cascade } | ||
| t.date :day, null: false | ||
| t.decimal :usage, null: false, default: 0 | ||
|
|
||
|
raphael-goetz marked this conversation as resolved.
|
||
| t.timestamps_with_timezone | ||
|
raphael-goetz marked this conversation as resolved.
|
||
|
|
||
| t.index %i[namespace_id flow_id day], unique: true | ||
| t.index %i[namespace_id day] | ||
| t.index %i[flow_id day] | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| b3cbb8e82f5a5fe001575d6c8ae8c27c15878108b650ec216f25aee8a00894f9 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.