Skip to content
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

Refactor contact archivation #1146

Merged
merged 22 commits into from
Sep 16, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
487613d
Refactor inactive contact archivation
Mar 28, 2019
d45e500
Archive contacts every day
Apr 2, 2019
6659c59
Change `log_domains.children` database column's type to `jsonb`
Apr 4, 2019
de8e663
Change `log_domains.children` database column's type to `jsonb`
Apr 4, 2019
73e9dd6
Resolve merge errors
karlerikounapuu Sep 2, 2020
e9f28a6
Merge branch 'refactor-contact-archivation' of https://github.com/int…
karlerikounapuu Sep 2, 2020
3c77566
Resolve some minor CC issues
karlerikounapuu Sep 2, 2020
7a24eab
Fix contact archivation tests
karlerikounapuu Sep 3, 2020
821c52a
Submit only object ID to DomainVersion linked methods instead of full…
karlerikounapuu Sep 3, 2020
6707ca1
Improve determining archivable contacts
karlerikounapuu Sep 3, 2020
28dea2f
Reference orphans_contacts_in_months directly from Settings
karlerikounapuu Sep 3, 2020
b2dab0d
Merge branch 'master' into refactor-contact-archivation
karlerikounapuu Sep 3, 2020
8161021
Add logging to archiving process
karlerikounapuu Sep 3, 2020
11fc484
Don't double check if contact can be archived when ran via Task
karlerikounapuu Sep 3, 2020
bbbb3d5
Improve logging
karlerikounapuu Sep 3, 2020
fa6ebd9
contacts:archive: delete orphaned once it's discovered / allow to sta…
karlerikounapuu Sep 4, 2020
dadb8ba
Send poll message to Registrar after it's contact has been archieved
karlerikounapuu Sep 4, 2020
5c7e07d
Use log() method instead of puts
karlerikounapuu Sep 4, 2020
a5b59f2
Fix CC issues
karlerikounapuu Sep 4, 2020
5330743
Test poll notification is sent to registrar after contact archivation
karlerikounapuu Sep 4, 2020
ab1fa90
Merge remote-tracking branch 'origin/master' into refactor-contact-ar…
karlerikounapuu Sep 16, 2020
7e9a325
Log deleted contacts to file, don't send poll message on initial cycle
karlerikounapuu Sep 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/models/concerns/contact/archivable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Concerns
module Contact
module Archivable
extend ActiveSupport::Concern

class_methods do
def archivable
unlinked.find_each.select(&:archivable?)
end
end

def archivable?(post: false)
inactive = inactive?

puts "Found archivable contact id(#{id}), code (#{code})" if inactive && !post

inactive
end

def archive(verified: false, notify: true)
unless verified
raise 'Contact cannot be archived' unless archivable?(post: true)
end

notify_registrar_about_archivation if notify
destroy!
end

private

def notify_registrar_about_archivation
registrar.notifications.create!(text: I18n.t('contact_has_been_archived',
contact_code: code,
orphan_months: Setting.orphans_contacts_in_months))
end

def inactive?
if DomainVersion.contact_unlinked_more_than?(contact_id: id, period: inactivity_period)
return true
end

DomainVersion.was_contact_linked?(id) ? false : created_at <= inactivity_period.ago
end

def inactivity_period
Setting.orphans_contacts_in_months.months
end
end
end
end
64 changes: 19 additions & 45 deletions app/models/contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Contact < ApplicationRecord
include Concerns::Contact::Transferable
include Concerns::Contact::Identical
include Concerns::Contact::Disclosable
include Concerns::Contact::Archivable
include Concerns::EmailVerifable

belongs_to :original, class_name: self.name
Expand Down Expand Up @@ -152,61 +153,17 @@ def search_by_query(query)
res.reduce([]) { |o, v| o << { id: v[:id], display_key: "#{v.name} (#{v.code})" } }
end

def find_orphans
where('
NOT EXISTS(
select 1 from domains d where d.registrant_id = contacts.id
) AND NOT EXISTS(
select 1 from domain_contacts dc where dc.contact_id = contacts.id
)
')
end

def find_linked
where('
EXISTS(
select 1 from domains d where d.registrant_id = contacts.id
) OR EXISTS(
select 1 from domain_contacts dc where dc.contact_id = contacts.id
)
')
end

def filter_by_states in_states
states = Array(in_states).dup
scope = all

# all contacts has state ok, so no need to filter by it
scope = scope.where("NOT contacts.statuses && ?::varchar[]", "{#{(STATUSES - [OK, LINKED]).join(',')}}") if states.delete(OK)
scope = scope.find_linked if states.delete(LINKED)
scope = scope.linked if states.delete(LINKED)
scope = scope.where("contacts.statuses @> ?::varchar[]", "{#{states.join(',')}}") if states.any?
scope
end

# To leave only new ones we need to check
# if contact was at any time used in domain.
# This can be checked by domain history.
# This can be checked by saved relations in children attribute
def destroy_orphans
STDOUT << "#{Time.zone.now.utc} - Destroying orphaned contacts\n" unless Rails.env.test?

counter = Counter.new
find_orphans.find_each do |contact|
ver_scope = []
%w(admin_contacts tech_contacts registrant).each do |type|
ver_scope << "(children->'#{type}')::jsonb <@ json_build_array(#{contact.id})::jsonb"
end
next if DomainVersion.where("created_at > ?", Time.now - Setting.orphans_contacts_in_months.to_i.months).where(ver_scope.join(" OR ")).any?
next if contact.linked?

contact.destroy
counter.next
STDOUT << "#{Time.zone.now.utc} Contact.destroy_orphans: ##{contact.id} (#{contact.name})\n" unless Rails.env.test?
end

STDOUT << "#{Time.zone.now.utc} - Successfully destroyed #{counter} orphaned contacts\n" unless Rails.env.test?
end

def admin_statuses
[
SERVER_UPDATE_PROHIBITED,
Expand Down Expand Up @@ -264,6 +221,23 @@ def registrant_user_direct_contacts(registrant_user)
.country.alpha2)
end

def linked
sql = <<-SQL
EXISTS(SELECT 1 FROM domains WHERE domains.registrant_id = contacts.id) OR
EXISTS(SELECT 1 FROM domain_contacts WHERE domain_contacts.contact_id =
contacts.id)
SQL

where(sql)
end

def unlinked
where('NOT EXISTS(SELECT 1 FROM domains WHERE domains.registrant_id = contacts.id)
AND
NOT EXISTS(SELECT 1 FROM domain_contacts WHERE domain_contacts.contact_id =
contacts.id)')
end

def registrant_user_company_contacts(registrant_user)
ident = registrant_user.companies.collect(&:registration_number)

Expand Down
15 changes: 15 additions & 0 deletions app/models/inactive_contacts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class InactiveContacts
attr_reader :contacts

def initialize(contacts = Contact.archivable)
@contacts = contacts
end

def archive(verified: false)
contacts.each do |contact|
puts "Archiving contact: id(#{contact.id}), code(#{contact.code})"
contact.archive(verified: verified)
yield contact if block_given?
end
end
end
37 changes: 37 additions & 0 deletions app/models/version/domain_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,41 @@ class DomainVersion < PaperTrail::Version
self.sequence_name = :log_domains_id_seq

scope :deleted, -> { where(event: 'destroy') }

def self.was_contact_linked?(contact_id)
sql = <<-SQL
SELECT
COUNT(*)
FROM
#{table_name}
WHERE
(children->'registrant') @> '#{contact_id}'
OR
(children->'admin_contacts') @> '#{contact_id}'
OR
(children->'tech_contacts') @> '#{contact_id}'
SQL

count_by_sql(sql).nonzero?
end

def self.contact_unlinked_more_than?(contact_id:, period:)
sql = <<-SQL
SELECT
COUNT(*)
FROM
#{table_name}
WHERE
created_at < TIMESTAMP WITH TIME ZONE '#{period.ago}'
AND (
(children->'registrant') @> '#{contact_id}'
OR
(children->'admin_contacts') @> '#{contact_id}'
OR
(children->'tech_contacts') @> '#{contact_id}'
)
SQL

count_by_sql(sql).nonzero?
end
end
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ en:
cant_match_version: 'Impossible match version with request'
user_not_authenticated: "user not authenticated"
actions: Actions
contact_has_been_archived: 'Contact with code %{contact_code} has been archieved because it has been orphaned for longer than %{orphan_months} months.'

number:
currency:
Expand Down
4 changes: 2 additions & 2 deletions config/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
runner 'DNS::Zone.generate_zonefiles'
end

every 6.months, at: '12:01am' do
runner 'Contact.destroy_orphans'
every :day, at: '12:01am' do
rake 'contacts:archive'
end

every :day, at: '12:10am' do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ChangeLogDomainsChildrenTypeToJsonb < ActiveRecord::Migration[6.0]
def change
change_column :log_domains, :children, 'jsonb USING children::jsonb'
end
end
6 changes: 4 additions & 2 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ CREATE TABLE public.log_domains (
object_changes json,
created_at timestamp without time zone,
session character varying,
children json,
children jsonb,
uuid character varying
);

Expand Down Expand Up @@ -4850,4 +4850,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200807110611'),
('20200811074839'),
('20200812090409'),
('20200812125810');
('20200812125810'),
('20200902131603');

27 changes: 27 additions & 0 deletions lib/tasks/contacts/archive.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace :contacts do
desc 'Archives inactive contacts'

task :archive, [:track_id] => [:environment] do |_t, args|
unlinked_contacts = contacts_start_point(args[:track_id])

counter = 0
puts "Found #{unlinked_contacts.count} unlinked contacts. Starting to archive."

unlinked_contacts.each do |contact|
next unless contact.archivable?

puts "Archiving contact: id(#{contact.id}), code(#{contact.code})"
contact.archive(verified: true)
counter += 1
end

puts "Archived total: #{counter}"
end

def contacts_start_point(track_id = nil)
puts "Starting to find archivable contacts WHERE CONTACT_ID > #{track_id}" if track_id
return Contact.unlinked unless track_id

Contact.unlinked.where("id > #{track_id}")
end
end
10 changes: 2 additions & 8 deletions test/mailers/previews/contact_mailer_preview.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
class ContactMailerPreview < ActionMailer::Preview
def email_changed
# Replace with `Contact.in_use` once https://github.com/internetee/registry/pull/1146 is merged
contact = Contact.where('EXISTS(SELECT 1 FROM domains WHERE domains.registrant_id = contacts.id)
OR
EXISTS(SELECT 1 FROM domain_contacts WHERE domain_contacts.contact_id =
contacts.id)')

contact = contact.where.not(email: nil, country_code: nil, ident_country_code: nil, code: nil)
.take
contact = Contact.linked
contact = contact.where.not(email: nil, country_code: nil, code: nil).first

ContactMailer.email_changed(contact: contact, old_email: 'old@inbox.test')
end
Expand Down
81 changes: 81 additions & 0 deletions test/models/contact/archivable_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'test_helper'

class ArchivableContactTest < ActiveSupport::TestCase
setup do
@contact = contacts(:john)
end

def test_contact_is_archivable_when_it_was_linked_and_inactivity_period_has_passed
DomainVersion.stub(:was_contact_linked?, true) do
DomainVersion.stub(:contact_unlinked_more_than?, true) do
assert @contact.archivable?
end
end
end

def test_contact_is_archivable_when_it_was_never_linked_and_inactivity_period_has_passed
Setting.orphans_contacts_in_months = 0
@contact.created_at = Time.zone.parse('2010-07-05 00:00:00')
travel_to Time.zone.parse('2010-07-05 00:00:01')

DomainVersion.stub(:was_contact_linked?, false) do
assert @contact.archivable?
end
end

def test_contact_is_not_archivable_when_it_was_never_linked_and_inactivity_period_has_not_passed
Setting.orphans_contacts_in_months = 5
@contact.created_at = Time.zone.parse('2010-07-05')
travel_to Time.zone.parse('2010-07-05')

DomainVersion.stub(:contact_unlinked_more_than?, false) do
assert_not @contact.archivable?
end
end

def test_contact_is_not_archivable_when_it_was_ever_linked_but_linked_within_inactivity_period
DomainVersion.stub(:was_contact_linked?, true) do
DomainVersion.stub(:contact_unlinked_more_than?, false) do
assert_not @contact.archivable?
end
end
end

def test_archives_contact
contact = archivable_contact

assert_difference 'Contact.count', -1 do
contact.archive
end
end

def test_unarchivable_contact_cannot_be_archived
contact = unarchivable_contact

e = assert_raises do
contact.archive
end
assert_equal 'Contact cannot be archived', e.message
end

private

def archivable_contact
contact = contacts(:john)
Setting.orphans_contacts_in_months = 0
DomainVersion.delete_all

other_contact = contacts(:william)
assert_not_equal other_contact, contact
Domain.update_all(registrant_id: other_contact.id)

DomainContact.delete_all

contact
end

def unarchivable_contact
Setting.orphans_contacts_in_months = 1188
@contact
end
end
Loading