feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
135
app/models/rkn_domain.go
Обычный файл
135
app/models/rkn_domain.go
Обычный файл
@@ -0,0 +1,135 @@
|
||||
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:]
|
||||
}
|
||||
Ссылка в новой задаче
Block a user