Учёт DEPO: задание SQL Server Agent — своё у каждой базы (bj2depo_<база>), имя в мастере
На одном сервере бывает несколько баз DEPO (боевая и тестовая): общее имя bj2depo давало ложную зелёную проверку — тестовой базе засчитывалось боевое задание, а своё не создавалось. - Имя задания — настройка DepoJobName; в мастере, пункт 3, поле «Имя задания SQL Server Agent», по умолчанию bj2depo_<имя базы> (меняется вместе с базой, пока его не поправили). Установки старше — bj2depo, как было. - Скрипт до любых изменений останавливается, если задание с этим именем обрабатывает другую базу или эту базу уже обрабатывает задание с другим именем (иначе двойная обработка событий). - Проверка задания сравнивает его базу (sp_help_jobstep) с базой bj. Синтаксис проверен на SQL Server без выполнения (SET PARSEONLY); проверка задания на боевом сервере — зелёная (bj2depo ↔ BJ_DEPO_DATA). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -743,7 +743,24 @@ func (s *Server) depoJobLoop(ctx context.Context) {
|
||||
|
||||
// ---- выбор способа обработки при настройке: проверки и создание джобы ----
|
||||
|
||||
const depoJobName = "bj2depo"
|
||||
// depoJobNameLegacy — имя задания у установок до 06.10.2026 (одно на сервер).
|
||||
const depoJobNameLegacy = "bj2depo"
|
||||
|
||||
// depoJobNameRe — допустимое имя задания SQL Server Agent (подставляется в скрипты).
|
||||
var depoJobNameRe = regexp.MustCompile(`^[A-Za-z0-9_.\-]{1,100}$`)
|
||||
|
||||
// depoJobName — имя задания SQL Server Agent этой установки. С 06.10.2026 задание называется по базе
|
||||
// (depoJobNameFor): на одном сервере бывает несколько баз DEPO (боевая и тестовая), и у каждой своё
|
||||
// задание. Пусто в настройках — установка старше, у неё задание bj2depo.
|
||||
func depoJobName(f FancyMSSQLSettings) string {
|
||||
if n := strings.TrimSpace(f.DepoJobName); n != "" {
|
||||
return n
|
||||
}
|
||||
return depoJobNameLegacy
|
||||
}
|
||||
|
||||
// depoJobNameFor — имя задания по умолчанию для базы db.
|
||||
func depoJobNameFor(db string) string { return "bj2depo_" + db }
|
||||
|
||||
// depoJobAgentScript — скрипт для администратора SQL (ручной вариант).
|
||||
func depoJobAgentScript(f FancyMSSQLSettings) string {
|
||||
@@ -751,7 +768,7 @@ func depoJobAgentScript(f FancyMSSQLSettings) string {
|
||||
EXEC dbo.sp_add_job @job_name = N'%[1]s', @enabled = 1, @description = N'bj2depo: обработка трансферных таблиц M2M (bj)';
|
||||
EXEC dbo.sp_add_jobstep @job_name = N'%[1]s', @step_name = N'%[1]s', @subsystem = N'TSQL', @database_name = N'%[2]s', @command = N'exec %[3]s;';
|
||||
EXEC dbo.sp_add_jobschedule @job_name = N'%[1]s', @name = N'%[1]s', @enabled = 1, @freq_type = 4, @freq_interval = 1, @freq_subday_type = 2, @freq_subday_interval = %[4]d, @active_start_time = 0;
|
||||
EXEC dbo.sp_add_jobserver @job_name = N'%[1]s';`, depoJobName, f.DepoDB(), depoJobProcName(f), depoJobEvery(f))
|
||||
EXEC dbo.sp_add_jobserver @job_name = N'%[1]s';`, depoJobName(f), f.DepoDB(), depoJobProcName(f), depoJobEvery(f))
|
||||
}
|
||||
|
||||
// depoJobAgentAdminScript — тот же набор для администратора SQL (вариант «создаёт администратор по
|
||||
@@ -784,7 +801,7 @@ IF EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'%[5]s' AND type
|
||||
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"))
|
||||
`, depoJobName(f), f.DepoDB(), depoJobProcName(f), depoJobEvery(f), pickStr(strings.TrimSpace(f.User), "bj_user"))
|
||||
}
|
||||
|
||||
// depoJobGrantScript — права для варианта «внутри bj».
|
||||
@@ -865,14 +882,23 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad
|
||||
runMsg sql.NullString
|
||||
now time.Time
|
||||
)
|
||||
err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName)).Scan(&jobEnabled, &now)
|
||||
err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName(f))).Scan(&jobEnabled, &now)
|
||||
if err == sql.ErrNoRows {
|
||||
return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
|
||||
return "джобы " + depoJobName(f) + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false
|
||||
}
|
||||
if err != nil {
|
||||
return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false
|
||||
}
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName)); err == nil {
|
||||
// задание с этим именем — той ли базы (на одном сервере у боевой и тестовой баз DEPO свои задания)
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobstep @job_name = @n", sql.Named("n", depoJobName(f))); err == nil {
|
||||
for _, r := range rows {
|
||||
if dbn := strings.TrimSpace(fmt.Sprint(r["database_name"])); dbn != "" && dbn != "<nil>" && !strings.EqualFold(dbn, f.DepoDB()) {
|
||||
return fmt.Sprintf("задание %s обрабатывает базу %s, а bj работает с базой %s — это задание другой базы. Задайте для этой базы своё имя задания (мастер, «Учёт DEPO», пункт 3) и подготовьте базу заново",
|
||||
depoJobName(f), dbn, f.DepoDB()), false
|
||||
}
|
||||
}
|
||||
}
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName(f))); err == nil {
|
||||
for _, r := range rows {
|
||||
if mapInt(r, "enabled") == 1 || !schedEnabled.Valid {
|
||||
schedEnabled = sql.NullInt64{Int64: mapInt(r, "enabled"), Valid: true}
|
||||
@@ -881,14 +907,14 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad
|
||||
}
|
||||
}
|
||||
}
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName)); err == nil && len(rows) > 0 {
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName(f))); err == nil && len(rows) > 0 {
|
||||
runDate = sql.NullInt64{Int64: mapInt(rows[0], "last_run_date"), Valid: true}
|
||||
runTime = sql.NullInt64{Int64: mapInt(rows[0], "last_run_time"), Valid: true}
|
||||
runStatus = sql.NullInt64{Int64: mapInt(rows[0], "last_run_outcome"), Valid: true}
|
||||
}
|
||||
if runStatus.Valid && runStatus.Int64 == 0 && runDate.Int64 != 0 {
|
||||
// причина — в сообщении последнего выполненного шага (история идёт от новых к старым)
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName)); err == nil {
|
||||
if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName(f))); err == nil {
|
||||
for _, r := range rows {
|
||||
if mapInt(r, "step_id") > 0 {
|
||||
runMsg = sql.NullString{String: fmt.Sprint(r["message"]), Valid: true}
|
||||
@@ -898,10 +924,10 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad
|
||||
}
|
||||
}
|
||||
if jobEnabled.Int64 != 1 {
|
||||
return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
|
||||
return "джоба " + depoJobName(f) + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false
|
||||
}
|
||||
if !schedEnabled.Valid || schedEnabled.Int64 != 1 {
|
||||
return "у джобы " + depoJobName + " нет включённого расписания — она не будет запускаться", false
|
||||
return "у джобы " + depoJobName(f) + " нет включённого расписания — она не будет запускаться", false
|
||||
}
|
||||
every := time.Duration(depoJobEvery(f)) * time.Second
|
||||
if subType.Int64 == 2 && subInt.Int64 > 0 { // каждые N секунд
|
||||
@@ -909,7 +935,7 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad
|
||||
} else if subType.Int64 == 4 && subInt.Int64 > 0 { // каждые N минут
|
||||
every = time.Duration(subInt.Int64) * time.Minute
|
||||
}
|
||||
sched := fmt.Sprintf("джоба %s включена, запускается каждые %s", depoJobName, humanEvery(every))
|
||||
sched := fmt.Sprintf("джоба %s включена, запускается каждые %s", depoJobName(f), humanEvery(every))
|
||||
if !runDate.Valid || runDate.Int64 == 0 {
|
||||
return sched + ", но ещё ни разу не запускалась — запущена ли служба SQL Server Agent?", false
|
||||
}
|
||||
@@ -992,12 +1018,12 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
|
||||
}
|
||||
defer mdb.Close()
|
||||
var n int
|
||||
if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil {
|
||||
if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName(f))).Scan(&n); err != nil {
|
||||
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)
|
||||
return fmt.Errorf("джоба %s уже существует", depoJobName(f))
|
||||
}
|
||||
for _, stmt := range strings.Split(depoJobAgentScript(f), "\n") {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
@@ -1008,7 +1034,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin
|
||||
return fmt.Errorf("%s → %s. Нужны права: %s", stmt[:min(40, len(stmt))], quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " "))
|
||||
}
|
||||
}
|
||||
log.Printf("depo-job: джоба SQL Agent %s создана", depoJobName)
|
||||
log.Printf("depo-job: джоба SQL Agent %s создана", depoJobName(f))
|
||||
depoGrantJobReader(ctx, mdb, f)
|
||||
return nil
|
||||
}
|
||||
@@ -1037,7 +1063,7 @@ type DepoJobView struct {
|
||||
}
|
||||
|
||||
func depoJobView(f FancyMSSQLSettings) DepoJobView {
|
||||
return DepoJobView{Name: depoJobName, Proc: depoJobProcName(f), Every: depoJobEvery(f), GrantScript: depoJobGrantScript(f), AgentRights: depoJobAgentRightsScript(f), AgentScript: depoJobAgentScript(f), AdminScript: depoJobAgentAdminScript(f)}
|
||||
return DepoJobView{Name: depoJobName(f), Proc: depoJobProcName(f), Every: depoJobEvery(f), GrantScript: depoJobGrantScript(f), AgentRights: depoJobAgentRightsScript(f), AgentScript: depoJobAgentScript(f), AdminScript: depoJobAgentAdminScript(f)}
|
||||
}
|
||||
|
||||
// ---- выгрузка истории тестов (TEST3, банк Зенит / робот) в трансферные таблицы ----
|
||||
|
||||
Reference in New Issue
Block a user