Update Dockerfile and Ruby files for improved dependency management and type safety

- Upgraded bundler version from 2.4.5 to 2.5.6 in Dockerfile for better compatibility.
- Updated gem versions for faiss and rails_live_reload to their latest versions.
- Modified Ruby files to enforce strict typing with Sorbet, enhancing type safety across various models and jobs.
- Added type signatures to methods in Inkbunny and FA domain models, ensuring better type checking and documentation.
- Improved job argument handling in Inkbunny jobs, including priority settings for deferred jobs.
- Refactored initialization logic in several models to ensure default states are set correctly.

These changes aim to enhance the overall stability and maintainability of the codebase.
This commit is contained in:
Dylan Knutson
2025-01-01 02:55:10 +00:00
parent 2d68b7bc15
commit 69ea16daf6
26 changed files with 135 additions and 46 deletions

View File

@@ -9,13 +9,13 @@ RUN \
cmake
WORKDIR /usr/src/app
RUN gem install bundler -v '2.4.5'
RUN gem install bundler -v '2.5.6'
COPY gems gems
WORKDIR /usr/src/app/gems/xdiff-rb
RUN bundle install
RUN bundle _2.5.6_ install
RUN rake compile
WORKDIR /usr/src/app/gems/rb-bsdiff
RUN bundle install
RUN bundle _2.5.6_ install
RUN rake compile
# Primary image
@@ -34,9 +34,9 @@ RUN \
libblas-dev liblapack-dev
# preinstall gems that take a long time to install
RUN MAKE="make -j12" gem install bundler -v '2.4.6' --verbose
RUN MAKE="make -j12" gem install faiss -v '0.2.5' --verbose
RUN MAKE="make -j12" gem install rails_live_reload -v '0.3.4' --verbose
RUN MAKE="make -j12" gem install bundler -v '2.5.6' --verbose
RUN MAKE="make -j12" gem install faiss -v '0.3.2' --verbose
RUN MAKE="make -j12" gem install rails_live_reload -v '0.3.6' --verbose
RUN bundle config --global frozen 1
# set up nodejs 18.x deb repo
@@ -58,7 +58,7 @@ COPY --from=native-gems /usr/src/app/gems/rb-bsdiff /gems/rb-bsdiff
WORKDIR /usr/src/app
COPY Gemfile Gemfile.lock ./
RUN bundle install
RUN bundle _2.5.6_ install
# install js dependencies
COPY package.json yarn.lock ./

View File

@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# Processes a page of submissions from the API search endpoint.
#
@@ -10,6 +10,8 @@ class Domain::Inkbunny::Job::ApiSearchPageProcessor
SUBMISSIONS_PER_PAGE = 100
MAX_LOOP_COUNT = 50
sig { returns(T::Array[Domain::Inkbunny::Post]) }
attr_reader :changed_posts
sig { void }

View File

@@ -57,6 +57,7 @@ module Domain::Inkbunny::Job
defer_job(
Domain::Inkbunny::Job::UserGalleryJob,
{ user: user, caused_by_entry: first_log_entry },
{ priority: 2 },
)
end
end

View File

@@ -110,6 +110,7 @@ module Domain::Inkbunny::Job
defer_job(
Domain::Inkbunny::Job::FileJob,
{ file: file, caused_by_entry: caused_by_entry },
{ priority: 1 },
)
end
post.save!

View File

@@ -1,7 +1,37 @@
# typed: strict
module HasIndexedPost
extend ActiveSupport::Concern
extend T::Helpers
extend T::Sig
abstract!
# Abstract methods that must be implemented by including class
sig { abstract.returns(T.nilable(IndexedPost)) }
def indexed_post
end
sig { abstract.params(post: T.nilable(IndexedPost)).void }
def indexed_post=(post)
end
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def created_at
end
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def posted_at
end
sig { abstract.returns(T::Boolean) }
def posted_at_changed?
end
included do
T.bind(
self,
T.all(T.class_of(ReduxApplicationRecord), T::Class[HasIndexedPost]),
)
has_one :indexed_post,
as: :postable,
dependent: :destroy,
@@ -9,16 +39,18 @@ module HasIndexedPost
autosave: true
before_create :ensure_indexed_post!
sig { returns(IndexedPost) }
def ensure_indexed_post!
self.indexed_post ||=
IndexedPost.new(created_at: self.created_at, postable: self)
end
before_save :ensure_indexed_post_posted_at!
sig { returns(T.nilable(IndexedPost)) }
def ensure_indexed_post_posted_at!
if self.posted_at_changed?
self.ensure_indexed_post!
self.indexed_post.posted_at = self.posted_at
T.must(self.indexed_post).tap { |ip| ip.posted_at = self.posted_at }
end
end
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::E621::Post < ReduxApplicationRecord
self.table_name = "domain_e621_posts"
@@ -12,7 +14,7 @@ class Domain::E621::Post < ReduxApplicationRecord
validates_presence_of(:e621_id, :state)
after_initialize do
self.state ||= :ok
self.state = :ok unless self.state.present?
self.state_detail ||= {}
self.flags_array ||= []
self.pools_array ||= []
@@ -35,30 +37,37 @@ class Domain::E621::Post < ReduxApplicationRecord
foreign_key: :e621_id,
optional: true
sig { returns(String) }
def to_param
self.e621_id.to_s
end
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
def tags_array
ta = super
return nil if ta.nil?
ta.is_a?(Hash) ? ta : { "general" => ta }
end
sig { returns(T.nilable(HttpLogEntry)) }
def index_page_http_log_entry
if state_detail["last_index_page_id"].present?
HttpLogEntry.find_by(id: state_detail["last_index_page_id"])
end
end
sig { returns(T.nilable(Addressable::URI)) }
def file_uri
Addressable::URI.parse(self.file_url_str) if self.file_url_str.present?
end
sig { returns(T.nilable(Time)) }
def e621_updated_at
str = state_detail["e621_updated_at"]
Time.parse(str) if str
end
sig { params(time: T.any(Time, String)).void }
def e621_updated_at=(time)
time = Time.parse(time) if time.is_a?(String)
state_detail["e621_updated_at"] = time.iso8601

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::E621::Tag < ReduxApplicationRecord
self.table_name = "domain_e621_tags"
has_many :taggings, class_name: "Domain::E621::Tagging", inverse_of: :tag

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::E621::Tagging < ReduxApplicationRecord
self.table_name = "domain_e621_taggings"
self.primary_key = %i[post_id tag_id]

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Fa::Fav < ReduxApplicationRecord
self.table_name = "domain_fa_favs"
self.primary_key = %i[user_id post_id]

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Fa::Follow < ReduxApplicationRecord
self.table_name = "domain_fa_follows"
self.primary_key = %i[follower_id followed_id]

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Fa::Post < ReduxApplicationRecord
self.table_name = "domain_fa_posts"
@@ -45,14 +47,17 @@ class Domain::Fa::Post < ReduxApplicationRecord
foreign_key: :post_id,
dependent: :destroy
sig { returns(String) }
def to_param
self.fa_id.to_s
end
sig { returns(T.nilable(Addressable::URI)) }
def file_uri
Addressable::URI.parse(self.file_url_str) if self.file_url_str
end
sig { params(uri: T.nilable(T.any(String, Addressable::URI))).void }
def file_uri=(uri)
if uri
uri = Addressable::URI.parse(uri)
@@ -63,6 +68,7 @@ class Domain::Fa::Post < ReduxApplicationRecord
end
end
sig { returns(T.nilable(Addressable::URI)) }
def thumbnail_uri
if self.state_detail["thumbnail_url_str"]
Addressable::URI.parse(self.state_detail["thumbnail_url_str"])
@@ -71,6 +77,7 @@ class Domain::Fa::Post < ReduxApplicationRecord
end
end
sig { params(uri: T.nilable(T.any(String, Addressable::URI))).void }
def thumbnail_uri=(uri)
if uri
uri = Addressable::URI.parse(uri)
@@ -81,10 +88,12 @@ class Domain::Fa::Post < ReduxApplicationRecord
end
end
sig { returns(T::Boolean) }
def scanned?
self.file_url_str.present?
end
sig { returns(T.nilable(Time)) }
def scanned_at
# at some point, `scanned_at` was populated to avoid having to look up the
# post's `last_submission_page` log entry, but we fall back to that
@@ -96,36 +105,35 @@ class Domain::Fa::Post < ReduxApplicationRecord
end
end
sig { params(time: T.nilable(Time)).void }
def scanned_at=(time)
unless time.nil?
unless time.is_a?(Time)
raise ArgumentError.new("time must be Time, was #{time.class}")
end
end
self.state_detail["scanned_at"] = time&.to_i
end
sig { override.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def posted_at
pa = super
return pa if pa
@posted_at ||=
begin
contents = guess_last_submission_page&.response&.contents
if contents
parser = Domain::Fa::Parser::Page.new(contents)
parser.submission.posted_date if parser.probably_submission?
end
begin
contents = guess_last_submission_page&.response&.contents
if contents
parser = Domain::Fa::Parser::Page.new(contents)
parser.submission.posted_date if parser.probably_submission?
end
end
end
sig { params(log_entry: T.nilable(HttpLogEntry)).void }
def last_submission_page=(log_entry)
self.log_entry_detail["last_submission_page_id"] = log_entry.id
self.log_entry_detail["last_submission_page_id"] = log_entry&.id
end
sig { returns(T.nilable(HttpLogEntry)) }
def last_submission_page
HttpLogEntry.find_by_id(self.log_entry_detail["last_submission_page_id"])
HttpLogEntry.find_by(id: self.log_entry_detail["last_submission_page_id"])
end
sig { returns(T.nilable(HttpLogEntry)) }
def guess_last_submission_page
last_submission_page ||
begin
@@ -141,6 +149,7 @@ class Domain::Fa::Post < ReduxApplicationRecord
end
end
sig { returns(T.nilable(String)) }
def description
content = super
return nil if content.nil? || content.blank?
@@ -149,30 +158,33 @@ class Domain::Fa::Post < ReduxApplicationRecord
# always empty and a <br><br>
lines = content.lines.map(&:strip).map(&:chomp)
if lines.length > 3
if lines[0] == "" && lines[1].start_with?("<a href=") &&
if lines[0] == "" && lines[1]&.start_with?("<a href=") &&
lines[2] == "<br><br>"
return lines[3..].join("\n")
return (lines[3..] || []).join("\n")
end
end
content
end
sig { returns(T::Boolean) }
def have_file?
self.file_id.present?
end
sig do
params(
submission: T.untyped,
first_seen_log_entry: T.nilable(HttpLogEntry),
).returns(T::Hash[String, T.untyped])
end
def self.hash_from_submission_parser_helper(
submission,
first_seen_log_entry: nil
)
creator =
begin
Domain::Fa::User.find_or_create_by!(
{ url_name: submission.artist_url_name },
) { |user| user.name = submission.artist }
rescue ActiveRecord::RecordNotFound => e
binding.pry
end
Domain::Fa::User.find_or_create_by!(
{ url_name: submission.artist_url_name },
) { |user| user.name = submission.artist }
{
fa_id: submission.id,

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Fa::PostFactor < ReduxApplicationRecord
self.table_name = "domain_fa_post_factors"
self.primary_key = "post_id"

View File

@@ -24,8 +24,6 @@ class Domain::Fa::UserAvatar < ReduxApplicationRecord
@file_model ||= BlobEntry.ensure(file_sha256) if file_sha256
end
before_validation { file_uri = Addressable::URI.parse(file_url_str) }
def file_uri
Addressable::URI.parse(file_url_str) unless file_url_str.blank?
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Fav < ReduxApplicationRecord
self.table_name = "domain_inkbunny_favs"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::File < ReduxApplicationRecord
self.table_name = "domain_inkbunny_files"
@@ -12,7 +14,7 @@ class Domain::Inkbunny::File < ReduxApplicationRecord
enum :state, %i[ok error]
after_initialize do
self.state ||= :ok
self.state = :ok unless self.state.present?
self.state_detail ||= {}
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Follow < ReduxApplicationRecord
self.table_name = "domain_inkbunny_follows"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Pool < ReduxApplicationRecord
self.table_name = "domain_inkbunny_pools"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::PoolJoin < ReduxApplicationRecord
self.table_name = "domain_inkbunny_pool_joins"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Post < ReduxApplicationRecord
self.table_name = "domain_inkbunny_posts"
include HasIndexedPost
@@ -38,11 +40,12 @@ class Domain::Inkbunny::Post < ReduxApplicationRecord
]
after_initialize do
self.state ||= :ok
self.state = :ok unless self.state.present?
self.state_detail ||= {}
self.ib_detail_raw ||= {}
end
sig { returns(String) }
def to_param
ib_post_id.to_s
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Tag < ReduxApplicationRecord
self.table_name = "domain_inkbunny_tags"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::Tagging < ReduxApplicationRecord
self.table_name = "domain_inkbunny_taggings"
end

View File

@@ -1,3 +1,5 @@
# typed: strict
class Domain::Inkbunny::User < ReduxApplicationRecord
self.table_name = "domain_inkbunny_users"
@@ -30,15 +32,18 @@ class Domain::Inkbunny::User < ReduxApplicationRecord
enum :avatar_state, %i[ok not_found error], prefix: :avatar
after_initialize do
self.state ||= :ok
self.state = :ok unless self.state.present?
self.state_detail ||= {}
self.avatar_state_detail ||= {}
end
sig { returns(T::Boolean) }
def due_for_gallery_scan?
scanned_gallery_at.blank? || scanned_gallery_at < 1.month.ago
s = scanned_gallery_at
s.blank? || s < 1.month.ago
end
sig { returns(String) }
def to_param
name
end

View File

@@ -1,3 +1,5 @@
# typed: true
class HttpLogEntry < ReduxApplicationRecord
include ImmutableModel
before_destroy { raise ActiveRecord::ReadOnlyRecord }
@@ -46,7 +48,7 @@ class HttpLogEntry < ReduxApplicationRecord
def response_size
if association(:response).loaded?
response.size
T.must(self.response).size
else
BlobEntry.where(sha256: response_sha256).pick(:size)
end

View File

@@ -1,4 +1,6 @@
class ReduxApplicationRecord < ActiveRecord::Base
extend T::Sig
self.abstract_class = true
logger.level = Logger::ERROR
end

View File

@@ -21,3 +21,6 @@ test:
tc *args:
bundle exec srb tc {{args}}
tapioca *args:
bundle _2.5.6_ exec tapioca {{args}}

View File

@@ -54,11 +54,6 @@ RSpec.describe Domain::Fa::Post do
post.scanned_at = time
expect(post.state_detail["scanned_at"]).to eq(time.to_i)
end
it "raises ArgumentError for non-Time values" do
expect { post.scanned_at = "2024-03-20" }.to raise_error(ArgumentError)
expect { post.scanned_at = Date.today }.to raise_error(ArgumentError)
end
end
describe "#scanned_at" do
@@ -69,7 +64,7 @@ RSpec.describe Domain::Fa::Post do
end
it "returns Time object from stored timestamp" do
time = Time.current
time = Time.now
post.scanned_at = time
expect(post.scanned_at).to be_within(1.second).of(time)
end