Учёт DEPO: шаг «Обработка» проверяет запуск джобы (есть, включена, расписание, запускалась недавно — по msdb), работа процедур (BJ_PING) — отдельной проверкой «Обработка событий»; логину bj — SQLAgentReaderRole для проверки
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -777,7 +777,12 @@ END
|
||||
ELSE
|
||||
PRINT N'Джоба %[1]s уже есть — не изменялась.';
|
||||
GO
|
||||
`, depoJobName, f.DepoDB(), depoJobProcName(f), depoJobEvery(f))
|
||||
|
||||
-- Логину bj — чтение списка джоб и их истории: bj проверяет, что джоба запускается по расписанию.
|
||||
IF USER_ID(N'%[5]s') IS NULL CREATE USER [%[5]s] FOR LOGIN [%[5]s];
|
||||
IF IS_ROLEMEMBER(N'SQLAgentReaderRole', N'%[5]s') = 0 ALTER ROLE SQLAgentReaderRole ADD MEMBER [%[5]s];
|
||||
GO
|
||||
`, depoJobName, f.DepoDB(), depoJobProcName(f), depoJobEvery(f), pickStr(strings.TrimSpace(f.User), "bj_user"))
|
||||
}
|
||||
|
||||
// depoJobGrantScript — права для варианта «внутри bj».
|
||||
@@ -807,11 +812,121 @@ func depoJobPing(ctx context.Context, db *sql.DB, wait time.Duration) (bool, str
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
return false, fmt.Sprintf("проверочное событие BJ_PING не обработано за %s. Проверьте по порядку: 1) в базе DEPO есть процедуры обработки (FTV_M2M_JOB и FTV_M2M_PROCESS_*) — без них джоба запускается и падает; 2) джоба bj2depo есть и включена, служба SQL Server Agent запущена (история джобы в SSMS покажет ошибку); 3) пользователь, от имени которого работают процедуры (SYSDBA), есть в базе", wait)
|
||||
return false, fmt.Sprintf("проверочное событие BJ_PING не обработано за %s — процедуры обработки не отработали. Проверьте: 1) в базе DEPO есть процедуры FTV_M2M_JOB и FTV_M2M_PROCESS_*; 2) пользователь, от имени которого они работают (SYSDBA), есть в базе; 3) ошибку последнего запуска джобы — в её истории в SSMS", wait)
|
||||
}
|
||||
|
||||
// depoJobCheck — проверка готовности выбранного способа обработки.
|
||||
func depoJobCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
|
||||
return depoJobRunnerCheck(ctx, rc, "", "")
|
||||
}
|
||||
|
||||
// depoJobRunnerCheck — запускалка обработки (05.10.2026: шаг «Обработка» проверяет только её, а не процедуры):
|
||||
// SQL Agent — джоба есть, включена, есть расписание и она запускалась недавно; «bj сам» — право EXECUTE.
|
||||
// adminUser/adminPass — разовый логин администратора (если введён), иначе — логин bj (нужен SQLAgentReaderRole в msdb).
|
||||
func depoJobRunnerCheck(ctx context.Context, rc *RuntimeConfig, adminUser, adminPass string) (string, bool) {
|
||||
f := rc.Snapshot().Fancy
|
||||
switch f.DepoJobMode {
|
||||
case "bj":
|
||||
db, err := depoEventsDB(rc)
|
||||
if err != nil {
|
||||
return "подключение к DEPO: " + err.Error(), false
|
||||
}
|
||||
defer db.Close()
|
||||
var n int
|
||||
_ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM fn_my_permissions(@p,'OBJECT') WHERE permission_name='EXECUTE'", sql.Named("p", depoJobProcName(f))).Scan(&n)
|
||||
if n == 0 {
|
||||
return "у логина " + f.User + " нет права EXECUTE на " + depoJobProcName(f) + " — выполните на сервере: " + strings.ReplaceAll(depoJobGrantScript(f), "\n", " "), false
|
||||
}
|
||||
return fmt.Sprintf("право на %s есть; bj будет вызывать её каждые %d с", depoJobProcName(f), depoJobEvery(f)), true
|
||||
case "sql-auto", "sql-manual":
|
||||
return depoAgentJobStatus(ctx, f, adminUser, adminPass)
|
||||
}
|
||||
return "способ обработки не выбран", false
|
||||
}
|
||||
|
||||
// depoAgentJobStatus — состояние джобы SQL Agent по msdb.
|
||||
func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, bool) {
|
||||
conn := f
|
||||
if strings.TrimSpace(adminUser) != "" {
|
||||
conn.User, conn.Password = strings.TrimSpace(adminUser), adminPass
|
||||
}
|
||||
mdb, err := fancyOpen(conn, "msdb")
|
||||
if err != nil {
|
||||
return "подключение к msdb: " + err.Error(), false
|
||||
}
|
||||
defer mdb.Close()
|
||||
var (
|
||||
jobEnabled, schedEnabled, subType, subInt sql.NullInt64
|
||||
runDate, runTime, runStatus sql.NullInt64
|
||||
runMsg sql.NullString
|
||||
now time.Time
|
||||
)
|
||||
err = mdb.QueryRowContext(ctx, `SELECT j.enabled, s.enabled, s.freq_subday_type, s.freq_subday_interval,
|
||||
h.run_date, h.run_time, h.run_status, h.message, GETDATE()
|
||||
FROM msdb.dbo.sysjobs j
|
||||
LEFT JOIN msdb.dbo.sysjobschedules js ON js.job_id = j.job_id
|
||||
LEFT JOIN msdb.dbo.sysschedules s ON s.schedule_id = js.schedule_id
|
||||
OUTER APPLY (SELECT TOP 1 run_date, run_time, run_status, message FROM msdb.dbo.sysjobhistory
|
||||
WHERE job_id = j.job_id AND step_id = 0 ORDER BY instance_id DESC) h
|
||||
WHERE j.name = @n`, sql.Named("n", depoJobName)).Scan(&jobEnabled, &schedEnabled, &subType, &subInt, &runDate, &runTime, &runStatus, &runMsg, &now)
|
||||
if err == sql.ErrNoRows {
|
||||
return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
|
||||
}
|
||||
if err != nil {
|
||||
return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false
|
||||
}
|
||||
if jobEnabled.Int64 != 1 {
|
||||
return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
|
||||
}
|
||||
if !schedEnabled.Valid || schedEnabled.Int64 != 1 {
|
||||
return "у джобы " + depoJobName + " нет включённого расписания — она не будет запускаться", false
|
||||
}
|
||||
every := time.Duration(depoJobEvery(f)) * time.Second
|
||||
if subType.Int64 == 2 && subInt.Int64 > 0 { // каждые N секунд
|
||||
every = time.Duration(subInt.Int64) * time.Second
|
||||
} else if subType.Int64 == 4 && subInt.Int64 > 0 { // каждые N минут
|
||||
every = time.Duration(subInt.Int64) * time.Minute
|
||||
}
|
||||
sched := fmt.Sprintf("джоба %s включена, запускается каждые %s", depoJobName, humanEvery(every))
|
||||
if !runDate.Valid || runDate.Int64 == 0 {
|
||||
return sched + ", но ещё ни разу не запускалась — запущена ли служба SQL Server Agent?", false
|
||||
}
|
||||
d, t := runDate.Int64, runTime.Int64
|
||||
last := time.Date(int(d/10000), time.Month(d/100%100), int(d%100), int(t/10000), int(t/100%100), int(t%100), 0, now.Location())
|
||||
age := now.Sub(last)
|
||||
limit := 3 * every
|
||||
if limit < time.Minute {
|
||||
limit = time.Minute
|
||||
}
|
||||
if age > limit {
|
||||
return fmt.Sprintf("%s, но последний запуск был %s (%s назад) — запущена ли служба SQL Server Agent?", sched, last.Format("02.01 15:04:05"), humanDur(age)), false
|
||||
}
|
||||
msg := fmt.Sprintf("%s; последний запуск %s (%d с назад)", sched, last.Format("15:04:05"), int(age.Seconds()))
|
||||
if runStatus.Int64 == 0 { // запуск есть, но обработка внутри упала — это не про джобу
|
||||
txt := strings.TrimSpace(runMsg.String)
|
||||
if r := []rune(txt); len(r) > 300 {
|
||||
txt = string(r[:300]) + "…"
|
||||
}
|
||||
msg += ". Запуск завершился ошибкой обработки — это уже не про джобу, а про процедуры (см. «Обработка событий»): " + txt
|
||||
}
|
||||
return msg, true
|
||||
}
|
||||
|
||||
func humanEvery(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%d с", int(d.Seconds()))
|
||||
}
|
||||
return fmt.Sprintf("%d мин", int(d.Minutes()))
|
||||
}
|
||||
|
||||
// depoJobReaderGrant — право логину bj видеть джобы и их историю (для проверки джобы из bj).
|
||||
func depoJobReaderGrant(f FancyMSSQLSettings) string {
|
||||
return fmt.Sprintf("USE msdb;\nIF USER_ID(N'%[1]s') IS NULL CREATE USER [%[1]s] FOR LOGIN [%[1]s];\nIF IS_ROLEMEMBER(N'SQLAgentReaderRole', N'%[1]s') = 0 ALTER ROLE SQLAgentReaderRole ADD MEMBER [%[1]s];", f.User)
|
||||
}
|
||||
|
||||
// depoProcCheck — работает ли сама обработка событий (процедуры FTV_M2M_*): таблицы, пользователь
|
||||
// процедур, ошибки обработчика, проверочное событие BJ_PING. Отдельно от запускалки (джобы).
|
||||
func depoProcCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
|
||||
f := rc.Snapshot().Fancy
|
||||
db, err := depoEventsDB(rc)
|
||||
if err != nil {
|
||||
@@ -819,7 +934,7 @@ func depoJobCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
|
||||
}
|
||||
defer db.Close()
|
||||
if !depoTableExists(ctx, db, "FTV_M2M_EVENTS") {
|
||||
return "в базе " + f.DepoDB() + " нет таблиц FTV_M2M* — создайте их: Настройка → Обмен Fancy → «Трансферные таблицы DEPO»", false
|
||||
return "в базе " + f.DepoDB() + " нет таблиц FTV_M2M*", false
|
||||
}
|
||||
if ok, msg := depoRunAsCheck(ctx, db, f); !ok {
|
||||
return msg, false
|
||||
@@ -828,37 +943,13 @@ func depoJobCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
|
||||
return fmt.Sprintf("обработчик за последние сутки не смог обработать записей: %d (последняя: %s %s — %s). Устраните причину и верните их в очередь: Обмен с DEPO → «Вернуть в очередь»",
|
||||
len(fails), fails[0].TableRu(), fails[0].Type, fails[0].Comment), false
|
||||
}
|
||||
switch f.DepoJobMode {
|
||||
case "bj":
|
||||
var n int
|
||||
_ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM fn_my_permissions(@p,'OBJECT') WHERE permission_name='EXECUTE'", sql.Named("p", depoJobProcName(f))).Scan(&n)
|
||||
if n == 0 {
|
||||
return "у логина " + f.User + " нет права EXECUTE на " + depoJobProcName(f) + " — выполните на сервере: " + strings.ReplaceAll(depoJobGrantScript(f), "\n", " "), false
|
||||
}
|
||||
start := time.Now()
|
||||
if f.DepoJobMode == "bj" {
|
||||
if _, err := execProc(ctx, db, depoJobProcName(f)); err != nil {
|
||||
return "вызов " + depoJobProcName(f) + ": " + quikErrorDetail(err), false
|
||||
}
|
||||
return fmt.Sprintf("право есть, процедура %s выполнена за %s; bj будет вызывать её каждые %d с", depoJobProcName(f), time.Since(start).Round(time.Millisecond), depoJobEvery(f)), true
|
||||
case "sql-auto", "sql-manual":
|
||||
// 1) джоба видна через msdb?
|
||||
var seen string
|
||||
if mdb, err := fancyOpen(f, "msdb"); err == nil {
|
||||
var enabled int
|
||||
if e := mdb.QueryRowContext(ctx, "SELECT enabled FROM msdb.dbo.sysjobs WHERE name=@n", sql.Named("n", depoJobName)).Scan(&enabled); e == nil {
|
||||
if enabled == 1 {
|
||||
seen = "джоба " + depoJobName + " есть и включена; "
|
||||
} else {
|
||||
seen = "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА; "
|
||||
}
|
||||
}
|
||||
mdb.Close()
|
||||
}
|
||||
// 2) реально ли обрабатывает: BJ_PING
|
||||
ok, msg := depoJobPing(ctx, db, 40*time.Second)
|
||||
return seen + msg, ok
|
||||
}
|
||||
return "способ обработки не выбран", false
|
||||
ok, msg := depoJobPing(ctx, db, 40*time.Second)
|
||||
return msg, ok
|
||||
}
|
||||
|
||||
// depoJobCreateAgent — создать джобу SQL Agent от имени логина bj (нужна роль SQLAgentOperatorRole).
|
||||
@@ -880,6 +971,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
|
||||
return fmt.Errorf("нет доступа к msdb (%s). Нужны права: %s", quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " "))
|
||||
}
|
||||
if n > 0 {
|
||||
depoGrantJobReader(ctx, mdb, f) // джоба осталась (msdb не восстанавливали) — права на проверку всё равно выдаём
|
||||
return fmt.Errorf("джоба %s уже существует", depoJobName)
|
||||
}
|
||||
for _, stmt := range strings.Split(depoJobAgentScript(f), "\n") {
|
||||
@@ -892,9 +984,26 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
|
||||
}
|
||||
}
|
||||
log.Printf("depo-job: джоба SQL Agent %s создана", depoJobName)
|
||||
depoGrantJobReader(ctx, mdb, f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// depoGrantJobReader — пока есть логин администратора: логину bj — чтение списка джоб (для проверки).
|
||||
func depoGrantJobReader(ctx context.Context, mdb *sql.DB, f FancyMSSQLSettings) {
|
||||
if !depoLoginRe.MatchString(f.User) {
|
||||
return
|
||||
}
|
||||
for _, q := range strings.Split(depoJobReaderGrant(f), "\n") {
|
||||
if strings.HasPrefix(q, "USE ") {
|
||||
continue
|
||||
}
|
||||
if _, err := mdb.ExecContext(ctx, q); err != nil {
|
||||
log.Printf("depo-job: право SQLAgentReaderRole логину %s: %s", f.User, quikErrorDetail(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DepoJobView — данные карточки «Где запускается обработка».
|
||||
type DepoJobView struct {
|
||||
Name, Proc, GrantScript, AgentRights, AgentScript string
|
||||
|
||||
Reference in New Issue
Block a user