«Настройка» без 10-секундного ожидания: статистика npr.history по первичному ключу

Каждый рендер «Настройки» ждал /section службы bj-quikexport, а та
считала min(ts) и count(*) за сутки по npr.history (18,5 млн строк,
индекса по ts нет): 6 + 27 с на 28, срез по таймауту 10 с. Теперь начало
истории — первая строка по id, строки за сутки/час — двоичный поиск по id
(история только дописывается): ~45 мс. То же в /admin/npr/api/freshness.

bj-server ждёт раздел QUIK-экспорта не дольше 3 с и иначе отдаёт последний
удачный; прогрев версий, пользователей ЛК и раздела после запуска.
«Обновления» перезагружают страницу, только если что-то изменилось.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-22 11:01:45 +03:00
parent 2aa682e97f
commit 077e542c1a
5 changed files with 97 additions and 6 deletions
+26 -2
View File
@@ -16,6 +16,7 @@ import (
"net/url"
"os"
"strings"
"sync"
"time"
)
@@ -59,14 +60,33 @@ func quikExportDo(ctx context.Context, rc *RuntimeConfig, method, uri string, bo
return quikExportHTTP.Do(req)
}
// qxSection — последний ответ службы: страница «Настройка» не ждёт её дольше qxSectionWait, а отдаёт
// последний удачный раздел (служба могла задуматься над статистикой базы).
var qxSection struct {
sync.Mutex
v QuikExportView
at time.Time
ro bool
}
const qxSectionWait = 3 * time.Second
func quikExportView(rc *RuntimeConfig, showRO bool) QuikExportView {
uri := "/section"
if showRO {
uri += "?reveal_ro=1"
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), qxSectionWait)
defer cancel()
resp, err := quikExportDo(ctx, rc, http.MethodGet, uri, nil, "")
if err != nil {
qxSection.Lock()
v, at, ro := qxSection.v, qxSection.at, qxSection.ro
qxSection.Unlock()
if !at.IsZero() && ro == showRO && time.Since(at) < 30*time.Minute {
return v
}
}
if err == nil {
b, rerr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
resp.Body.Close()
@@ -76,7 +96,11 @@ func quikExportView(rc *RuntimeConfig, showRO bool) QuikExportView {
case resp.StatusCode != http.StatusOK:
err = fmt.Errorf("служба ответила %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
default:
return QuikExportView{HTML: template.HTML(b), Ind: resp.Header.Get("X-BJ-Indicator")}
v := QuikExportView{HTML: template.HTML(b), Ind: resp.Header.Get("X-BJ-Indicator")}
qxSection.Lock()
qxSection.v, qxSection.at, qxSection.ro = v, time.Now(), showRO
qxSection.Unlock()
return v
}
}
return QuikExportView{Ind: "err", HTML: template.HTML(`<h1>QUIK — Экспорт биржевой информации</h1>`) + moduleMessage(
+6
View File
@@ -48,6 +48,12 @@ type adminTemplates struct {
// registerSetup вешает /admin/setup и /admin/setup/* (POST) на mux.
func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service) {
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}, svc: svc}
go func() { // прогрев: версии компонентов, пользователи ЛК, раздел QUIK-экспорта
time.Sleep(5 * time.Second)
systemVersions(rc)
lkUsersForSetup(rc)
quikExportView(rc, false)
}()
mux.HandleFunc("/admin/setup", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method", http.StatusMethodNotAllowed)
@@ -189,11 +189,15 @@
try{ tried=+sessionStorage.getItem('bj-upd-auto')||0; }catch(e){}
if(!force && (now-last<600 || now-tried<600)) return; // проверяли меньше 10 минут назад (или уже пробовали сами)
try{ sessionStorage.setItem('bj-upd-auto',String(Math.floor(now))); }catch(e){}
busy=true; var b=box.querySelector('.stc'); if(b){ b.className='stc stc-run'; var t=b.querySelector('b'); if(t) t.textContent='Проверяю обновления…'; }
busy=true; var b=box.querySelector('.stc'), t=b&&b.querySelector('b'), was=b&&[b.className,t?t.textContent:''];
if(b){ b.className='stc stc-run'; if(t) t.textContent='Проверяю обновления…'; }
function restore(){ busy=false; if(b){ b.className=was[0]; if(t) t.textContent=was[1]; } }
fetch('/admin/setup/update/check',{method:'POST',headers:{'X-BJ-Ajax':'1'},cache:'no-store'}).then(function(r){return r.json()}).then(function(d){
box.dataset.last=String(Math.floor(Date.now()/1000));
// ничего нового — страницу не перерисовываем (полный рендер «Настройки» дорогой)
if(d && d.ok && (d.has_update?'1':'0')===box.dataset.has && (!d.has_update || box.textContent.indexOf(d.available||'\u0000')>=0)){ restore(); return; }
location.reload(); // состояние компонентов — с сервера
}).catch(function(){ busy=false; location.reload(); });
}).catch(function(){ restore(); });
}
box.addEventListener('click',function(e){ if(e.target.closest('[data-upd-check]')) check(true); });
document.querySelectorAll('.settings-nav [data-sec="update"]').forEach(function(b){ b.addEventListener('click',function(){ setTimeout(function(){ check(false); },0); }); });