Files
worker/app/models/rkn_domain.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

136 строки
4.5 KiB
Go

package models
import (
"strings"
"rsgit.ru/rsmon/rsmon/app/models/concerns"
)
// RknDomain is one row of the locally cached ru-blocked domains list. The
// `Domain` field carries a lowercased domain string and is the unique key.
type RknDomain struct {
concerns.Model
Domain string `gorm:"size:255;uniqueIndex;not null" json:"domain"`
}
// TableName pins the underlying table name so GORM migrations and raw SQL
// (used by IsRknDomainBlocked and the rkn updater) agree.
func (RknDomain) TableName() string { return "rkn_domains" }
// ReplaceRknDomains deletes every existing row and inserts the given domains
// in batches. Caller-supplied domains are lower-cased and de-duplicated, and
// blank entries are dropped. The whole operation runs in a single
// transaction so readers see either the old set or the new set — there is no
// in-between state where the table is half-flushed.
//
// Designed to be called once per parser-updater tick (default every 6h); the
// uniqueIndex on Domain guarantees idempotent re-runs even when the caller
// forgets to filter.
func ReplaceRknDomains(domains []string) error {
if domains == nil {
domains = []string{}
}
deduped := make([]string, 0, len(domains))
seen := make(map[string]struct{}, len(domains))
for _, d := range domains {
d = strings.ToLower(strings.TrimSpace(d))
if d == "" {
continue
}
if _, ok := seen[d]; ok {
continue
}
seen[d] = struct{}{}
deduped = append(deduped, d)
}
tx := DB().Begin()
if tx.Error != nil {
return tx.Error
}
// Step 1 — wipe the existing cache. Using a scoped Where("1 = 1") Delete
// instead of TRUNCATE so the advisory lock taken by Migrate() does not
// become a bottleneck and so any in-flight foreign-key checks against
// rkn_domains remain satisfied (the table has no FKs today, but this
// matches the convention used by Drop()).
if err := tx.Where("1 = 1").Delete(&RknDomain{}).Error; err != nil {
_ = tx.Rollback().Error
return err
}
// Step 2 — bulk-insert in chunks of 1000 rows. CreateInBatches runs N
// multi-row INSERT statements, which for the typical ~30k ru-blocked
// entries is ~3ms per batch — significantly cheaper than per-row
// Create() in tight loops (the previous AddRknDomain implementation).
const batchSize = 1000
for start := 0; start < len(deduped); start += batchSize {
end := start + batchSize
if end > len(deduped) {
end = len(deduped)
}
rows := make([]RknDomain, 0, end-start)
for _, d := range deduped[start:end] {
rows = append(rows, RknDomain{Domain: d})
}
if err := tx.CreateInBatches(rows, batchSize).Error; err != nil {
_ = tx.Rollback().Error
return err
}
}
return tx.Commit().Error
}
// IsRknDomainBlocked returns true iff `domain` (or its root label, or any
// parent suffix already recorded as `*.parent.tld`) is present in the
// rkn_domains table.
//
// Matching rules — see checks/crkn/rkn_init.go for the original logic we
// consolidate here:
// 1. Exact match against the stored domain string.
// 2. Root-domain match (last two labels of the input) — covers the case
// where the user passed a subdomain but the upstream only lists the
// apex.
// 3. Suffix match (`stored LIKE '%' || input || ?`) — covers the case
// where the user passed the apex (or a higher-level label) but the
// upstream lists a child subdomain.
//
// All three checks are combined into a single SQL statement via OR so the
// table is scanned at most once and the SQL planner can pick a single
// index access path.
func IsRknDomainBlocked(domain string) (bool, error) {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain == "" {
return false, nil
}
rootDomain := rootDomainOf(domain)
// Build the suffix patterns once. Note: every ".X" entry in the table
// (i.e. a domain that begins with a dot) matches any subdomain whose
// suffix is domain.
suffixPattern := "%." + domain
var count int64
err := DB().Raw(
"SELECT COUNT(*) FROM rkn_domains WHERE domain = ? OR domain = ? OR domain LIKE ?",
domain, rootDomain, suffixPattern,
).Scan(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// rootDomainOf returns the last two labels of `domain` (e.g. "a.b.c" → "b.c").
// Returns `domain` unchanged when it has fewer than three labels, because a
// one- or two-label input IS already the apex/root domain.
func rootDomainOf(domain string) string {
i := strings.LastIndex(domain, ".")
if i < 0 {
return domain
}
j := strings.LastIndex(domain[:i], ".")
if j < 0 {
return domain
}
return domain[j+1:]
}