Release expired domain reservations daily - #2965
Open
OlegPhenomenon wants to merge 3 commits into
Open
OlegPhenomenon wants to merge 3 commits into
OlegPhenomenon wants to merge 3 commits into
Conversation
Add ReservedDomain.release_expired, run by cron every day at 00:35, which removes reservations whose expire_at has passed. Permanent reservations (expire_at NULL) are kept. Each record is locked and re-checked before removal. The reason is stored in updator_str and whodunnit before destroy, so the PaperTrail destroy version in log_reserved_domains carries the process, reason and timestamp. Lazy removal on availability check reuses the same path. A failing record is logged and reported to Airbrake without stopping the run. WHOIS refresh is enqueued after commit. Add index on expire_at. Closes #2963
maricavor
approved these changes
Oct 5, 2026
mmeest
approved these changes
Oct 5, 2026
The expired reservation was created one day before the real current time but released at a fixed past moment, so the test failed once the real date moved past it.
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #2963
What
Expired domain reservations are now removed by a daily cron job instead of relying only on lazy removal during Business Registry availability checks.
ReservedDomain.release_expiredremoves every reservation withexpire_at < now.expire_at IS NULL, e.g. created manually in admin) are never removed.log_reserved_domains(PaperTrail): process, reason and timestamp.reserved_domains.expire_atfor the cleanup query.How it works
For every expired reservation (
find_each, batches of 1000):SELECT ... FOR UPDATE) and expiry is re-checked, so a reservation extended in the meantime is not removed.PaperTrail.request(whodunnit: audit):update!(updator_str: audit)— creates anupdateversion, persists the reason;destroy!— creates adestroyversion whoseobject['updator_str']andwhodunnitcontain the reason.UpdateWhoisRecordJob.perform_later(name, 'reserved')refreshes WHOIS (after_destroy→after_destroy_commit, so the job never sees the row before it is deleted).Audit string format (built in one place,
ReservedDomain#release_audit_message, so #2962 can switch it to a structured format later):The second form is written by the existing lazy removal (
destroy_if_expiredinBusinessRegistry::DomainAvailabilityCheckerService), which now goes through the same code path.Error handling: a failing record is logged (stdout →
log/cron.log, andRails.logger.error→ application log) with its id, name and exception, its transaction is rolled back and the run continues with the next record. A record already deleted by a concurrent process is skipped silently.Deployment / setup
1. Migration
bundle exec rails db:migrate20261002120000_add_index_to_reserved_domains_on_expire_atcreates the index withalgorithm: :concurrently(no table lock,disable_ddl_transaction!). Rollback:bundle exec rails db:rollback(drops the index).2. Crontab
The job is defined in
config/schedule.rb(whenever) inside the@cron_group == 'registry'block:The crontab is not updated automatically by the code change — it must be regenerated on the registry (admin) server after deploy:
Only servers deployed with
cron_group=registryget this job;epp,registrar,registrantgroups do not.Preview what will be written to crontab without installing it:
Expected line:
3. Timezone
wheneveruses the server system time;expire_atis calculated inTime.zone(Tallinn). With #2961 / #2964 reservations expire at23:59:59Tallinn time, so on a server running in Tallinn time the job removes them ~35 minutes after expiry. If the server runs in UTC, the job runs at 00:35 UTC (03:35 Tallinn) — still correct, only the window is longer. Change the time inconfig/schedule.rbif needed.Manual run / troubleshooting
Local Docker environment:
Output (stdout →
log/cron.logwhen run by cron):failed > 0means some records stayed in the table; details are inlog/cron.logand the Rails application log. They will be retried on the next run.Audit trail queries
Tests
Covered in
test/models/reserved_domain_test.rb:expire_at = NULLrows kept; returned count;expire_at == cutoffis not removed;update+destroyversions with reason/process/timestamp inobject['updator_str']andwhodunnit;Business registry availability check;Also ran:
test/services/business_registry/,test/integration/api/business_registry/,test/models/free_domain_reservation_holder_test.rb,test/models/reserve_domain_invoice_test.rb,test/models/domain_test.rb— all green.Related
expire_at= 23:59:59 of the last day) andexpire_atfor paid reservations created by the billing callback. Without Feature/2961 reservation full day expiry #2964 paid reservations created via the callback have noexpire_atand are never removed by this job.CHANGELOG.mdandtest/models/reserved_domain_test.rb(both branches append) — keep both sides.Files
app/models/reserved_domain.rb—expiredscope,release_expired,release_if_expired,after_destroy_commitconfig/schedule.rb— daily job at 00:35db/migrate/20261002120000_add_index_to_reserved_domains_on_expire_at.rb,db/structure.sql— indextest/models/reserved_domain_test.rb— testsCHANGELOG.md