•  ánh sáng không có thực

Xây dựng một công cụ tổng hợp nội dung để giải trí và kiếm lợi nhuận?

Một ứng dụng đầy đủ giúp thu thập, xử lý và trình bày nội dung từ web...trên web.

Tại sao?

quá tải thông tin ? Ngày nay thật tệ, rất nhiều nguồn thông tin có tín hiệu thấp dẫn đến nhiễu, việc thu hẹp "nguồn cấp dữ liệu" của bạn để bạn không bị choáng ngợp là điều hơi khó. Một công cụ lọc thông tin và trình bày nó ở định dạng vừa dễ hiểu vừa nhanh chóng sẽ rất hữu ích. Đây là lý do tại sao tôi xem xét tổng hợp nội dung một lĩnh vực thường xanh cho sự gián đoạn. Nó đang và sẽ luôn là (miễn là internet miễn phí và tự do ngôn luận) là một cơ hội kinh doanh tốt. Đây là một trong những trường hợp mà tất cả là về việc thực thi (và không có gì về ý tưởng).

Quản lý kỳ vọng

Phải nói rằng, ứng dụng của tôi cuối cùng không thực hiện bất kỳ bộ lọc thực tế nào. Trong thực tế nó chỉ đơn giản là uẩn nội dung từ web. Điều này là do tôi đã không xây dựng người dùng trong đó và có rất ít động lực để thực hiện lọc nếu nó không thể được điều chỉnh cho mỗi người dùng.

Kiến trúc

Sơ đồ kiến ​​trúc: sơ đồ tổng hợp nội dungThực sự có rất nhiều vòng kết nối trong đó!...bạn biết...vi mô...dịch vụ không? Các ứng dụng mà tôi đã tạo là "trình quét" và "máy chủ", trong khi "nhà xuất bản" chỉ là một thói quen được nhúng trong máy chủ. "Tìm kiếm" và "proxy" là các công cụ bên ngoài thực hiện công việc của chúng. "Frontend" không có gì đặc biệt, là sự kết hợp giữa js và css đi kèm với webpack.

Vì có nhiều phần chuyển động khác nhau nên tôi sẽ đi theo dòng chảy của nội dung, bắt đầu từ khi nội dung được xem lần đầu tiên.

cái cạp

Quá trình cạo được thực hiện trong...bạn đoán xem, con trăn. Tuy nhiên, không có mô-đun "cào" đặc biệt nào được sử dụng.

Bạn cạo cái gì?

Việc quyết định loại bỏ nội dung nào phụ thuộc vào danh mục nội dung. Chúng tôi gọi các danh mục là "chủ đề".

Cạo xảy ra liên tục, đó là một con quỷ. Mã giả của vòng lặp chính trông như thế này và nó được cấu hình mỗi trang web:

xuất bản

Khi chúng tôi có nội dung để xuất bản, chúng tôi phải quyết định xuất bản nội dung gì và tần suất xuất bản. Tôi không nghĩ ra bất kỳ mánh khóe nào ở đây, bởi vì như tôi đã đề cập trước đó, việc chọn hiển thị cái gì là phụ thuộc vào người dùng. Vì vậy, chúng tôi chỉ xuất bản từ mới nhất đến cũ nhất , với lý do rằng thứ gì đó mà chúng tôi thu thập gần đây có liên quan hơn, đó là LIFO xếp hàng. Xuất bản mặc dù khá phụ thuộc vào các công cụ python, nhưng lại xảy ra trong nim, bởi vì nó chạy liền kề với máy chủ, cũng nằm trong nim.

logic xuất bản

Việc xuất bản diễn ra liên tục và giống như việc cạo có những khoảng thời gian nhàn rỗi, việc xuất bản cũng vậy, ngược lại với việc cạo. Khi chúng tôi cạo, chúng tôi chậm lại khi chúng tôi có một đủ dài bộ đệm của các bài báo chưa xuất bản, với việc xuất bản, chúng tôi làm chậm lại khi bộ đệm của chúng tôi bắt đầu thu hẹp quá nhiều. Bằng cách này, cùng với việc cạo, phải luôn có một số nội dung được công bố thỉnh thoảng trong tương lai. Logic xuất bản thực tế:

phục vụ

Chúng tôi thiết lập các công việc để vừa cạo nội dung xuất bản, tất cả những gì còn lại là phục vụ nội dung đó.

máy chủ web

Sau khi có đã thử khác nhau trang web may chủ, vì các lỗi khác nhau mà tôi đã giải quyết bọ cạp.

Xử lý một yêu cầu

bộ định tuyến

Chúng tôi sử dụng nim triển khai khớp mẫu để khớp với một bộ ảnh chụp regex. Đây là biểu thức chính quy hoàn toàn không phải là RESTful:

const
  rxend = "(?=/+|(?=[?].*)|$)"
  rxAmp = fmt"(/+amp{rxend})"
  rxLang = "(/[a-z]{2}(?:-[A-Z]{2})?" & fmt"{rxend})" # split to avoid formatting regex `{}` usage
  rxTopic = fmt"(/+.*?{rxend})"
  rxPage = fmt"(/+(?:[0-9]+|s|g|feed\.xml|sitemap\.xml){rxend})"
  rxArt = fmt"(/+.*?{rxend})"
  rxPath = fmt"{rxAmp}?{rxLang}?{rxTopic}?{rxPage}?{rxArt}?"

rxPath hiển thị tất cả các nút có thể có mà một đường dẫn có thể có. Sau đó, định tuyến của chúng tôi trông giống như:

let capts = uriTuple(reqCtx.url.path)
case capts:
  of (topic: ""): # homepage...
  of (topic: "assets"): # assets
  of (topic: "i"): # images
  of (topic: "robots.txt"): # robots.txt
  of (page: "sitemap.xml"): # sitemap for topics
  of (art: "index.xml"): # sitemap index for topic pages
  etc...

Nó không đẹp, nhưng việc không phụ thuộc vào bất kỳ bộ định tuyến cụ thể nào cho phép tôi trao đổi máy chủ web cấp dưới mà không gặp nhiều phiền phức khi thử nghiệm. Là nó hiệu suất? Không rõ! Chưa thực hiện bất kỳ điểm chuẩn nào so sánh nó với thứ gì khác. Tuy nhiên, mùi của regex có thể có lỗi và thực tế là thứ tự của các trường hợp có vấn đề.

Đợi tí...

Có rất nhiều thứ mà chúng tôi thực hiện trên mỗi yêu cầu trước khi định tuyến trang thực tế:

ban đầu chúng tôi thiết lập mã dọn dẹp (vớidefer: ) cái mà Nên đảm bảo không có rò rỉ xảy ra.

defer:
  # FIXME: is this cleanup required?
  var futs: seq[Future[void]]
  let resp =
    if ctx.response.issome: ctx.response.get
    else: nil
  if not resp.isnil and not resp.connection.isnil:
    futs.add resp.connection.closeWait()
  if not ctx.isnil:
    if not ctx.connection.isnil:
      futs.add ctx.connection.closeWait()
    futs.add ctx.closeWait()
  await allFutures(futs)

Chúng tôi kiểm tra xem chuỗi có được khởi tạo hay không:

initThread()

Điều này thực sự chỉ nên chạy một lần (nó đặt một bool toàn cầu sau khi khởi tạo để kiểm tra) và có thể được thực hiện bên ngoài trình xử lý yêu cầu. Nhưng những gì thực sự được khởi tạo? Vâng ... khá nhiều thứ! Về cơ bản, chúng tôi (ab) sử dụng các hằng số toàn cầu yêu cầu khởi tạo, ngoài ra một số trong số này không thực sự liên quan đến luồng, vì chúng khởi tạo bộ nhớ trên heap và được chia sẻ trên luồng

if threadInitialized:
  debug "thread: already initialized"
  return
debug "thread: base"
initThreadBase()
debug "thread: sonic"
initSonic() # Must be on top
debug "thread: http"
initHttp()
debug "thread: html"
initHtml()
debug "thread: ldj"
initLDJ()
debug "thread: feed"
initFeed()
debug "thread: img"
startImgFlow()
debug "thread: lsh"
startLsh()
debug "thread: mimes"
initMimes()
# ... and other stuff

Sau đó, chúng tôi phân tích các tham số

var
  relpath = ctx.rawPath
  page: string
  rqlocked: bool
relpath.removeSuffix('/')
debug "handling: {relpath:.120}"

handleParams()

Chúng ta sử dụng tham số để làm gì? CácParamKey loại enum mô tả nó:

type
  ParamKey = enum
    none,
    q, p, # sonic
    c, # cache
    d, # delete
    t,  # translations
    u # imgUrls

chúng tôi làm bộ nhớ đệm siêu nhỏ cho các yêu cầu để mọi yêu cầu được lưu vào bộ đệm theo bộ(path, query, accetEncoding) , mã hóa là bắt buộc vì chúng tôi có thể phục vụ cả hai nội dung (không) nén. Một bối cảnh yêu cầu trông như thế này:

let reqCtx = reqCtxCache.lcheckOrPut(reqCacheKey):
  let reqCtx {.gensym.} = new(ReqContext)
  block:
    let l = newAsyncLock()
    checkNil(l):
      reqCtx.lock = l
  reqCtx.url = move url
  reqCtx.params = params
  reqCtx.file = reqCtx.url.path.fp
  reqCtx.key = hash(reqCtx.file)
  reqCtx.rq = initTable[ReqId, HttpRequestRef]()
  new(reqCtx.respBody)
  reqCtx
if handleTranslation():
  return

Tại sao điều này cũng được thực hiện trước khi định tuyến? Theo mặc định, chúng tôi phục vụ các trang được dịch một phần. Chúng tôi kém :( và các bản dịch dựa trên các dịch vụ miễn phí, nhưng chúng tôi không thể chịu được thời gian tải quá lâu, vì vậy chúng tôi chạy bản dịch bị trì hoãn trong khi chúng tôi phục vụ trang được dịch chỉ với các đoạn trích đã được lưu trong bộ nhớ cache trong cơ sở dữ liệu bản dịch của chúng tôi.

Tại thời điểm này, có định tuyến, được bao bọc trong một ngoại lệ sao cho nếu phục vụ đúng trang không thành công, chúng tôi sẽ đưa ra một503 . phát hành một503 ngụ ý rằng chúng tôi đã cố định tuyến một url hợp lệ nhưng chúng tôi không thể tạo trang. Đối với các url không hợp lệ, chúng tôi đưa ra một301 chuyển hướng ngụ ý rằng url không hợp lệ. Chúng tôi phục vụ 11 loại url khác nhau:

Giới thiệu về kết xuất

Kết xuất trang được xử lý từ phía nim bằng cách sử dụng karax

Bố cục trang chung

Trang web bao gồm một thanh cố định trên cùng hiển thị:

Vì đây là thiết kế đáp ứng nên khi chế độ xem nhỏ hơn, thanh trên cùng chỉ giữ hộp tìm kiếm trong khi phần còn lại xuất hiện trong thanh bên có thể chuyển đổi.

RSS

Đây là một chức năng ví dụ, hiển thị những gì chúng tôi làm khi một bài đăng mới được xuất bản để cập nhật nguồn cấp dữ liệu:

proc update*(tfeed: Feed, topic: string, newArts: seq[Article], dowrite = false) =
    ## Load existing feed for given topic and update the feed (in-memory)
    ## with the new articles provided, it does not write to storage.
    checkNil tfeed
    let
        chann = tfeed.findel("channel")
        itms = chann.drainChannel
        arl = itms.len
        narl = newArts.len

    debug "rss: newArts: {narl}, previous: {arl}"
    let
        fill = RSS_N_ITEMS - arl
        rem = max(0, narl - fill)
        shrinked = if (rem > 0 and arl > 0):
                       itms[0..<(max(0, arl-rem))]
                   else: itms
    debug "rss: articles tail len {len(shrinked)}, newarts: {len(newArts)}"
    assert shrinked.len + narl <= RSS_N_ITEMS, fmt"shrinked: {shrinked.len}, newarticles: {narl}"
    for a in newArts:
        chann.add articleItem(a)
    for itm in shrinked:
        chann.add itm
    if dowrite:
   
        pageCache[][topic.feedKey] = tfeed.toXmlString

Sơ đồ trang web

Đây là cốt lõi của việc thêm url vào sơ đồ trang web:

template addUrlToFeed(getLoc, getLocLang) =
  if unlikely(nEntries > maxEntries):
      warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
      break
  let
      url = newElement("url")
      loc = newElement("loc")
  loc.add getLoc().escape.newText
  url.add loc
  addLangs(url, getLocLang)
  result.add url

proc buildTopicPagesSitemap*(topic: string): Future[XmlNode] {.async.} =
    initSitemapIndex()
    await syncTopics()
    var nEntries = 0
    let done = await topicDonePages(topic)
    template langUrl(lang): untyped {.dirty.} = $(WEBSITE_URL / lang / topic / pages[n])
    withPyLock:
        # add the most recent articles first (pages with higher idx)
        let pages = pybi[].list(done.keys()).to(seq[string])
        for n in countDown(pages.len - 1, 0):
          if not (await isEmptyPage(topic, pages[n].parseInt, false)):
            discard sitemapUrl(topic, pages[n]).sitemapEl

template addArticleToFeed() =
  template baseUrl(): untyped =
    getArticleUrl(a, topic)

  template langUrl(lang): untyped =
    getArticleUrl(a, topic, lang)

  if not a.isValidArticlePy:
      continue

  addUrlToFeed(baseUrl, langUrl)

proc buildTopicSitemap(topic: string): Future[XmlNode] {.async.} =
    initUrlSet()
    await syncTopics()
    let done = await topicDonePages(topic)
    var nEntries = 0
    withPyLock:
        # add the most recent articles first (pages with higher idx)
        for pagenum in countDown(len(done) - 1, 0):
            if unlikely(nEntries > maxEntries):
                warn "Number of URLs for sitemap of topic: {topic} exceeds limit! {nEntries}/{maxEntries}"
                break
            checkTrue pagenum in done, "Mismatching number of pages"
            for a in done[pagenum]:
                addArticleToFeed()

mẫu

Chúng tôi không sử dụng công cụ tạo mẫu vì hầu hết quá trình kết xuất được thực hiện bằng karax, nhưng đối với các trang như ToS, chúng tôi sử dụng các mẫu tệp, trong đó chúng tôi chỉ cần thay thế một loạt các biến, chẳng hạn như mộtenvsubst yêu cầu.

proc pageFromTemplate*(tpl, lang, amp: string): Future[string] {.async.} =
  var txt = await readfileAsync(ASSETS_PATH / "templates" / tpl & ".html")
  let (vars, title, desc) =
    case tpl:
      of "dmca": (tplRep, "DMCA", fmt"dmca compliance for {WEBSITE_DOMAIN}")
      of "tos": (ppRep, "Terms of Service",
          fmt"Terms of Service for {WEBSITE_DOMAIN}")
      of "privacy-policy": (ppRep, "Privacy Policy",
          fmt"Privacy Policy for {WEBSITE_DOMAIN}")
      else: (tplRep, tpl, "")
  txt = multiReplace(txt, vars)
  let
    slug = slugify(title)
    page = await buildPage(title = title, content = txt, wrap = true)
  checkNil(page):
    let processed = await processPage(lang, amp, page, relpath = tpl)
    checkNil(processed, fmt"failed to process template {tpl}, {lang}, {amp}"):
      return processed.asHtml(minify_css = (amp == ""))

trang bài viết

Khi chúng tôi hiển thị các trang như trang chủ/chủ đề và các trang được đánh số, chúng tôi phải hiển thị danh sách các bài viết, chức năng này được gọi trong một vòng lặp cho số lượng bài viết chúng tôi muốn hiển thị:

import htmlparser
proc articleEntry(ar: Article, topic = ""): Future[VNode] {.async.} =
  if ar.topic == "" and topic != "":
    ar.topic = topic
  let relpath = getArticlePath(ar)
  try:
    return buildHtml(article(class = "entry")):
      h2(class = "entry-title", id = ar.slug):
        a(href = relpath):
          text ar.title
      tdiv(class = "entry-info"):
        span(class = "entry-author"):
          text ar.getAuthor & ", "
        time(class = "entry-date", datetime = ($ar.pubDate)):
          italic:
            text format(ar.pubDate, "dd/MMM")
      tdiv(class = "entry-tags"):
        if ar.tags.len == 0:
          span(class = "entry-tag-name"):
            a(href = (await nextAdsLink()), target = "_blank"):
              icon("i-mdi-tag")
              text "none"
        else:
          for t in ar.tags:
            if likely(t.isSomething):
              span(class = "entry-tag-name"):
                a(href = (await nextAdsLink()), target = "_blank"):
                  icon("i-mdi-tag")
                  text t
      buildImgUrl(ar)
      tdiv(class = "entry-content"):
        verbatim(articleExcerpt(ar))
        a(class = "entry-more", href = relpath):
          text "[continue]"
      hr()
  except Exception as e:
    logexc()
    warn "articles: entry creation failed."
    raise e

proc buildShortPosts*(arts: seq[Article], topic = ""): Future[
    string] {.async.} =
  for a in arts:
    result.add $(await articleEntry(a, topic))

Lưu ý làm thế nào trong một số dòng "quảng cáo" leo trong X)

danh sách chủ đề

Ở thanh trên cùng, chúng tôi hiển thị danh sách chủ đề, đây là nội dung in ra:

proc topicsList*(ucls: string; icls: string; small: static[
    bool] = true): Future[VNode] {.async.} =
  result = newVNode(VNodeKind.ul)
  result.setAttr("class", ucls)
  let topics = await loadTopics(-MENU_TOPICS) # NOTE: the sign is negative, we load the most recent N topics
  result.add buildHtml(tdiv(class = "topics-shadow"))
  var topic_slug, topic_name: string
  var isEmpty: bool
  for i in 0..<topics.len:
    withPyLock:
      (topic_slug, topic_name) = ($topics[i][0], $topics[i][1])
      isEmpty = isEmptyTopic(topic_slug)
    if isEmpty:
      continue
    let liNode = buildHtml(li(class = fmt"{icls}")):
      # tdiv(class = "mdc-icon-button__ripple") # not used without material icons
      a(href = ($(WEBSITE_URL / topic_slug)), title = topic_name,
          class = "mdc-ripple-button"):
        tdiv(class = "mdc-ripple-surface  mdc-ripple-upgraded")
        when small:
          # only use the first letter
          text $topic_name.runeAt(0).toUpper # loadTopics iterator returns pyobjects
        else:
          text topic_name
      when small:
        br()
      else:
        span(class = "separator")
    result.add liNode

Có một số lớp thiết kế vật liệu được mã hóa cứng có mùi ở đây. Thành thật mà nói, các thành phần thiết kế material design của google rất tệ.

Cuối bài đăng

Chân trang bài đăng xuất hiện ở dưới cùng bên phải của trang bài viết (tính bằng ltr) và nó thực sự chỉ in ngày xuất bản.

proc postFooter(pubdate: Time): VNode =
  let dt = inZone(pubdate, utc())
  buildHtml(tdiv(class = "post-footer")):
    time(datetime = ($dt)):
      text "Published date: "
      italic:
        text format(dt, "dd MMM yyyy")

Trích đoạn

Khi xây dựng các mục bài viết, chúng tôi có thể cần các đoạn trích nếu không có bản tóm tắt.

proc articleExcerpt(a: Article): string =
  let alen = len(a.content) - 1
  let maxlen = min(alen, ARTICLE_EXCERPT_SIZE)
  if maxlen == alen:
    return a.content
  else:
    let runesize = runeLenAt(a.content, maxlen)
    # If article contains html tags, the excerpt might have broken html
    return parseHtml(a.content[0..maxlen+runesize]).innerText & "..."

wtf làparseHtml làm gì ở đây? Đó là trường hợp chúng tôi cho phép html bên trong nội dung bài viết (nhưng chỉ một số thẻ), đây là một tùy chọn từ trafilatura mô-đun python, chúng tôi tiếp tục bật vì nó có thể ảnh hưởng đến định dạng bài viết. Chúng ta cũng phải cẩn thận về việc chunking các chuỗi utf-8...

thu nhỏ

Nhiệm vụ cuối cùng sau khi chế tạo karaxVNode tree là để kết xuất các byte. Cây nếu có tiền tố là tiêu đề html và được thu nhỏ tùy ý.

proc asHtml*(data: string ; minify: static[bool] = true; minify_css: bool = true): string =
  let html = "<!DOCTYPE html>" & "\n" & data
  sdebug "html: raw size {len(html)}"
  result = when minify:
             html.minifyHtml(minify_css = false,
                             minify_js = false,
                             keep_closing_tags = true,
                             do_not_minify_doctype = true,
                             keep_spaces_between_attributes = true,
                             ensure_spec_compliant_unquoted_attribute_values = true)
           else:
             html
  sdebug "html: minified size {len(result)}"

Việc thu nhỏ được xử lý bởi rút gọn-html mà chúng tôi đã ràng buộc bằng cách sử dụng c2nim , tệp ràng buộc chứa:

proc minify*(code: cstring,
             do_not_minify_doctype = false,
             ensure_spec_compliant_unquoted_attribute_values = false,
             keep_closing_tags = true,
             keep_comments = false,
             keep_html_and_head_opening_tags = true,
             keep_spaces_between_attributes = false,
             minify_css = true,
             minify_js = true,
             remove_bangs = false,
             remove_processing_instructions = true): cstring {.importc: "minify".}

proc minifyHtml*(tree: VNode): string = $minify(($tree).cstring)
proc minifyHtml*(data: string): string = $minify(data.cstring)
template minifyHtml*(data: string, args: varargs[untyped]): string =
    $minify(data.cstring, args)

Nhưng để xây dựng, chúng tôi phải cung cấp các thư viện tĩnh, thêm dòng này vàonim.cfg

--passL:"$PROJECT_DIR/src/rust/target/release/libminify_html_c.a"

Ý tôi là... đó là con đường của tôi khi tôi xây dựng thư viện rút gọn btw không thực sự có chức năng c bên ngoài mà nim có thể sử dụng, vì vậy chúng tôi phải tự viết nó.

use minify_html::{Cfg, minify as minify_html_native};
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn minify(
    code: *const c_char,
    do_not_minify_doctype: bool,
    ensure_spec_compliant_unquoted_attribute_values: bool,
    keep_closing_tags: bool,
    keep_comments: bool,
    keep_html_and_head_opening_tags: bool,
    keep_spaces_between_attributes: bool,
    minify_css: bool,
    minify_js: bool,
    remove_bangs: bool,
    remove_processing_instructions: bool,
) -> *const c_char {

    let code = unsafe { CStr::from_ptr(code) };
    let code_vec = code.to_bytes();

    let cfg = Cfg {
        do_not_minify_doctype,
        ensure_spec_compliant_unquoted_attribute_values,
        keep_closing_tags,
        keep_comments,
        keep_html_and_head_opening_tags,
        keep_spaces_between_attributes,
        minify_css,
        minify_js,
        remove_bangs,
        remove_processing_instructions,
    };

    let minified = minify_html_native(code_vec, &cfg);

    let s = unsafe { CString::from_vec_unchecked(minified).into_raw() };
    return s;
}

Nimpy và nhiệm vụ xóa rác không gặp sự cố

Các ràng buộc Python cho nim phải tự do loại bỏ các đối tượng python. Vấn đề là chúng ta phải kiểm soát khi nim thực hiện GC. Thư viện nhanh nhẹn giả định rằng GIL luôn bị khóa (nó khóa nó ngay từ đầu), để có thể tự do gọi tới python bất cứ khi nào. Nhưng chúng tôi mở khóa gil để cho phép một threadpool python chạy mã trong khi nim chạy các nội dung khác. Nếu python GIL luôn bị khóa bởi nim thì threadpool sẽ không hoạt động trong hầu hết thời gian.

when defined(pyAsync):
  type
    PyGilObj = object
      lock: ThreadLock
      currentLockHolder: int
      state: PyGILState_STATE
    PyGil = ptr PyGilObj

  var pyGil*: PyGil
  var pyGilLock*: ThreadLock
  var pyMainThread: PyThreadState
  proc initPyGil*() =
    assert PyGILState_Check()
    pyGil = create(PyGilObj)
    pyGil.currentLockHolder = getThreadID()
    pyGil.lock = newThreadLock()
    pyGilLock = pyGil.lock
    pyMainThread = PyEval_SaveThread()

  proc acquire*(gil: PyGil): Future[void] {.async.} =
    await gil.lock.acquire
    let id = getThreadId()
    gil.currentLockHolder = id
    gil.state = Py_GILState_Ensure()

  proc tryAcquire*(gil: PyGil): bool =
    if gil.lock.tryAcquire():
      let id = getThreadId()
      gil.currentLockHolder = id
      gil.state = Py_GILState_Ensure()
      return true

  proc release*(gil: PyGil) {.inline.} =
    doassert gil.currentLockHolder == getThreadId(), "Can't release gil lock from a different thread."
    doassert gilLocked()
    Py_GILState_Release(gil.state)
    gil.lock.release

Điều này cho phép cách thực thi mã python giữ GIL, nhưng chỉ trên luồng hiện tại. Việc triển khai để lấy/giải phóng GIL trên các luồng nim khác nhau yêu cầu gọi các hàm abi python C khác nhau, vì GIL là một mutex. Sau đó, chúng tôi gọi python bằng mẫu này:

template withPyLock*(code): untyped =
  {.locks: [pyGil].}:
    try:
      # echo getThreadId(), " -- ", getCurrentProcessId(), " -- ", procName()
      await pygil.acquire()
      code
    except:
      raise getCurrentException()
    finally:
      # echo getThreadId(), " -- ", getCurrentProcessId(),  " -- unlocked"
      pygil.release()

Chúng tôi sử dụng tính năng bảo vệ và khóa nim để đảm bảo các loại python chỉ được truy cập khi GIL được giữ. Tuy nhiên, điều này yêu cầu xác định pyobjects với bảo vệ:

macro pyObjPtr*(defs: varargs[untyped]): untyped =
  result = newNimNode(nnkStmtList)
  for d in defs:
    let
      name = d[0]
      def = d[1]
    result.add quote do:
      let `name` {.guard: pyGil.} = create(PyObject)
      `name`[] = `def`

Vì vậy, tôi có thể làm:

pyObjPtr(myVar, pyimport("datetime").datetime))

Và bất cứ khi nào tôi gọimyVar chứa đối tượng datetime, tôi phải bọc nó như thế này:

withPyLock():
  myVar.fromunixtimestamp(1)

Bây giờ chúng ta có thể khóa gil khi chúng ta phải chạy GC, ghi đè nhanh lênPyObject hàm hủy với cái này:

var garbage: seq[PPyObject]

proc `=destroy`*(p: var PyObject) =
  if pygil.tryAcquire:
    if not p.rawPyObj.isnil:
      decRef p.rawPyObj
      p.rawPyObj = nil
    while garbage.len > 1:
      var pp = garbage.pop() # TODO: Does this leak a pointer?
      if not pp.isnil:
        decRef pp
      pp = nil
    pygil.release
  else:
    if not p.rawPyObj.isnil:
      garbage.add p.rawPyObj

Khóa chúng tôi sử dụng bên trong hàm hủy không phải là khóaAsyncLock vì điều đó sẽ quá đắt, và chúng tôi đừng luôn khóa, vì điều đó sẽ gây ra tình trạng chết hàng! Nếu chúng tôi không thể khóa gil, chúng tôi sẽ trì hoãn việc thu thập và giữ con trỏ python thô cho đến khi chúng tôi có thể xóa nó. Thành thật mà nói, tôi không biết liệu điều này có gây ra các dạng vấn đề khác hay không, nhưng có vẻ như nó hoạt động đủ tốt.

Chúng tôi có một mô-đun nim được gọi làpyutils.nim ví dụ như một loạt các công cụ nim<>python:

from utils import withLocks
proc pyhasAttr*(o: PyObject; a: string): bool {.withLocks: [pyGil].} = pybi[].hasattr(
    o, a).to(bool)

proc pyclass(py: PyObject): PyObject {.inline, withLocks: [pyGil].} =
  pybi[].type(py)

proc pytype*(py: PyObject): string =
  py.pyclass.getattr("__name__").to(string)

proc pyisbool*(py: PyObject): bool {.withLocks: [pyGil].} =
  return pybi[].isinstance(py, PyBoolClass[]).to(bool)

proc pyisnone*(py: PyObject): bool {.gcsafe, withLocks: [pyGil].} =
  return py.isnil or pybi[].isinstance(py, PyNoneClass[]).to(bool)

Cái này được sử dụng khá nhiều:

proc pyget*[T](py: PyObject; k: string; def: T = ""): T =
  try:
    let v = py.callMethod("get", k)
    if pyisnone(v):
      return def
    else:
      return v.to(T)
  except:
    pyErrClear()
    if pyisnone(py):
      return def
    else:
      return py.to(T)

Cái này được sử dụng khi chúng tôi đã lên lịch cho một công việc python và chúng tôi muốn đợi nó hoàn thành một cách không đồng bộ:

proc pywait*(j: PyObject): Future[PyObject] {.async, gcsafe.} =
  var rdy: bool
  var res: PyObject
  while true:
    withPyLock:
      checkNil(j)
      rdy = j.callMethod("ready").to(bool)
    if rdy:
      withPyLock:
        checkNil(j)
        res = j.callMethod("get")
      break
    await sleepAsync(250.milliseconds)
  withPyLock:
    if (not res.isnil) and (not pyisnone(res)) and (not pyErrOccurred()):
      return res
    else:
      raise newException(ValueError, "Python job failed.")

Liên kết async python thích hợp sẽ yêu cầu hoàn thành một tương lai không đồng bộ nim từ python khi kết thúc công việc theo lịch trình của python, điều mà chúng tôi không làm vì chúng tôi chưa xem xét đủ sâu để xử lý các đối tượng nim từ python.

khuếch đại

Chúng tôi hỗ trợ google amp, vì vậy chúng tôi tạo ra các trang amp tương thích với amp. Chúng tôi không nhắm đến hỗ trợ 1:1. Trên thực tế, chúng tôi sử dụng tất cả javascript mà chúng tôi có và chỉ phục vụ html/css. Ngay cả khi đó, chúng tôi phải cẩn thận để không thêm các thuộc tính tùy chỉnh vào thẻ html hoặc chỉ các thẻ html tùy chỉnh, amp như vậy là không tốt... Đối với chuyển đổi trang amp tự động, chúng tôi xử lýheadbody gắn thẻ khác nhau.

proc processHead(inHead: VNode, outHead: VNode, level = 0) {.async.} =
  var canonicalUnset = level == 0
  debug "iterating over {inHead.kind}"
  for el in inHead.preorder(withStyles = true):
    case el.kind:
      of VNodeKind.text, skipNodes:
        continue
      of VNodeKind.style:
        if el.len > 0:
          el[0].text.maybeStyle
      of VNodeKind.link:
        if canonicalUnset and el.isLink(canonical):
          outHead.add el
          canonicalUnset = false
        elif el.isLink(stylesheet) and (not ("flags-sprite" in el.getattr("href"))):
          await el.fetchStyle()
        elif el.isLink(preload) and el.getattr("as") == "style":
          await el.fetchStyle()
        else:
          outHead.add el
      of VNodeKind.script:
        if el.getAttr("type") == $ldjson:
          outHead.add el
      of VNodeKind.meta:
        if (el.getAttr("name") == "viewport") or (el.getAttr("charset") != ""):
          continue
        else:
          outHead.add el
      of VNodeKind.verbatim:
        let data = el.toXmlNode
        if data.kind == xnElement:
          if data.tag == "noscript":
            processNoScript()
          elif data.tag == "script":
            continue
          elif data.tag == "style":
            if data.len > 0:
              data[0].text.maybeStyle
          else:
            outHead.add el
      of VNodekind.noscript:
        processNoScript()
      else:
        debug "amphead: adding element {el.kind} to outHead."
        outHead.add el

Tất cả các kiểu được hợp nhất thành một tập lệnh nội tuyến duy nhất, những gì được giữ lại làlink các thẻ không phải là style/jscript, như lang. Thẻ tập lệnh choldljson, meta thẻ. Verbatim xử lý các nút được nghĩa đen , chúng ta phải chuyển đổi chúng thànhXmlNode (có nghĩa là phân tích cú pháp) và xử lý chính xác. Phần thân quy trình cũng tương tự, chúng tôi giữ một số thẻ, xóa các thẻ khác, đổi tên các thẻ khác:

template process(el: VNode, after: untyped): bool =
  var isprocessed = true
  case el.kind:
    of skipNodes: discard
    of VNodeKind.link:
      if el.isLink(stylesheet):
        await el.fetchStyle()
      else:
        outBody.add el
    of VNodeKind.style:
      el.text.maybeStyle
      el.text = ""
    of VNodeKind.script:
      if el.getAttr("type") == $ldjson:
        outHead.add el
      el.text = ""
    of VNodeKind.form:
      el.setAttr("amp-form", "")
    else:
      isprocessed = false
  if isprocessed:
    after
  isprocessed

Cácform thẻ được thay thế bằngamp-form , amp có nhiều thẻ này...

Chúng tôi phải đảm bảo rằng các kiểu nội tuyến nằm trong độ dài chính xác:

styleStr = styleStr
  # .join("\n")
  # NOTE: the replacement should be ordered from most frequent to rarest
  # # remove troublesome animations
  .replace(pre"""\s*?@(\-[a-zA-Z]+-)?keyframes\s+?.+?{\s*?.+?({.+?})+?\s*?}""", "")
  # # remove !important hints
  .replace(pre"""!important""", "")
  # remove charset since not allowed
  .replace(pre"""@charset\s+\"utf-8\"\s*;?/i""", "")

if unlikely(styleStr.len > CSS_MAX_SIZE):
  raise newException(ValueError, fmt"Style size above limit for amp pages. {styleStr.len}")

Thế hệ bộ khuếch đại của chúng tôi không bao gồm đầy đủ thông số kỹ thuật của bộ khuếch đại, nhưng nó phù hợp với nội dung của chúng tôi (thông qua bản dùng thử và lỗi :S).

Bất cứ khi nào một bài báo được xuất bản, nó sẽ được nhập vào cơ sở dữ liệu âm thanh, cơ sở dữ liệu âm thanh xử lý "bộ sưu tập", "nhóm" và "đối tượng"; Chúng tôi định nghĩa một bộ sưu tập là một trang web, vì vậy mỗi trang web muốn triển khai trình tổng hợp nội dung đều có bộ sưu tập của riêng mình. chúng tôi không sử dụngbuckets , mặc dù chúng tôi có thể coi mỗi chủ đề là một nhóm, điều này sẽ thu hẹp tìm kiếm quá nhiều, vì vậy mọi trang web chỉ có một nhóm "mặc định" và mỗi đối tượng của nhóm là một bài viết (có thể thuộc các chủ đề khác nhau).

proc push*(capts: UriCaptures, content: string) {.async.} =
  ## Push the contents of an article page to the search database
  ## NOTE: NOT thread safe
  var ofs = 0
  while ofs <= content.len:
    let view = content[ofs..^1]
    let key = join([capts.topic, capts.page, capts.art], "/")
    let cnt = runeSubStr(view, 0, min(view.len, bufsize - key.len))
    ofs += cnt.len
    if cnt.len == 0:
      break
    try:
      let lang = await capts.lang.toISO3
      var pushed: bool
      var j: PyObject
      withPyLock:
        j = pySched[].apply(
          pySonic[].push,
          WEBSITE_DOMAIN,
          "default", # TODO: Should we restrict search to `capts.topic`?
          key,
          cnt,
          lang = if capts.lang != "en": lang else: ""
          )
      j = await j.pywait()
      withPyLock:
        pushed = not pyisnone(j) and j.to(bool)
      when not defined(release):
        if not pushed:
          capts.addToBackLog()
          break
    except Exception:
      logexc()
      debug "sonic: couldn't push content, \n {capts} \n {key} \n {cnt}"
      when not defined(release):
        capts.addToBackLog()
        block:
          var f: File
          try:
            await pushLock[].acquire
            f = open("/tmp/sonic_debug.log", fmWrite)
            write(f, cnt)
          finally:
            pushLock[].release
            if not f.isnil:
              f.close()
      break

Khi đẩy nội dung vào âm thanh, chúng tôi phải chia dữ liệu thành nhiều phần, độ dài tối đa được biết khi kết nối. Đôi khi, việc nhập dữ liệu dường như có lỗi vì dữ liệu dường như không thể xử lý một số ký tự cụ thể. Trong trường hợp máy chủ âm thanh bị hỏng bằng cách nào đó, chúng tôi cũng có chức năng nhập lại tất cả nội dung:

proc pushAllSonic*() {.async.} =
  await syncTopics()
  var total, c, pagenum: int
  let pushLog = await readPushLog()
  if pushLog.len == 0:
    withPyLock:
      discard pySonic[].flush(WEBSITE_DOMAIN)
  defer:
    withPyLock:
      discard pySonic[].consolidate()
  for (topic, state) in topicsCache:
    if topic notin pushLog:
      pushLog[topic] = %0
    await pygil.acquire
    defer: pygil.release
    let done = state.group[]["done"]
    for page in done:
      pagenum = ($page).parseint
      c = len(done[page])
      if pushLog[topic].to(int) >= pagenum:
        continue
      var futs: seq[Future[void]]
      for n in 0..<c:
        let ar = done[page][n]
        if ar.isValidArticlePy:
          var relpath = getArticlePath(ar, topic)
          relpath.removeSuffix("/")
          let
            capts = uriTuple(relpath)
            content = ar.pyget("content").sanitize
          echo "pushing ", relpath
          futs.add push(capts, content)
          total.inc
      pygil.release
      await allFutures(futs)
      pushLog[topic] = %pagenum
      await writePushLog(pushLog)
      await pygil.acquire
  info "Indexed search for {WEBSITE_DOMAIN} with {total} objects."

Dịch

Bản dịch là một câu chuyện khá lộn xộn. Tôi đang triển khai trình bao bọc dịch lần thứ 4 (!), sau khi đã viết bằng php, hãy đi và Julia , điều này cũng được viết bằng nim. Các biến thể php/go ngày nay hơi bị hỏng, trong khi biến thể julia được sử dụng tích cực cho blog này. Tuy nhiên, để đạt được độ trễ thấp cho máy chủ web, cách triển khai dịch trong julia không phù hợp lắm với dịch vụ thời gian thực (nó dịch các tệp tĩnh) và dù sao thì việc thêm julia làm phụ thuộc khi chúng ta đã có python sẽ là một vấn đề lớn. yêu cầu.

Vì vậy, tôi đã phải triển khai một mô-đun dịch thuật mới trong nim. Trên thực tế, mô-đun dịch nim ban đầu trông rất giống với triển khai julia, nơi chúng tôi đang dịch các tệp tĩnh[1] . Sau đó, khi máy chủ web bắt đầu hình thành, tôi chuyển nó để dịch các nút karax theo yêu cầu. Điều này cho phép dịch từng trang web đúng lúc theo yêu cầu.

template translateVbtm(node: VNode, q: QueueDom) =
  assert node.kind == VNodeKind.verbatim
  let tree = ($node).parseHtml() # FIXME: this should be a conversion, but the conversion doesn't preserve whitespace??
  if tree.kind == xnElement and tree.tag == "document":
    tree.tag = "div"
  takeOverFields(tree.toVNode, node)
  translateIter(node, vbtm = false)

template translateIter(otree; vbtm: static[bool] = true) =
  for el in otree.preorder():
    case el.kind:
      of vdom.VNodeKind.text:
        if el.text.isEmptyOrWhitespace:
          continue
        if isTranslatable(el):
          translate(q.addr, el, srv)
      else:
        let t = el.kind
        if t in tformsTags:
          getTForms(dom)[t](el, file_path, url_path, pair)
        if t == VNodeKind.a:
          if el.hasAttr("href"):
            rewriteUrl(el, rewrite_path, hostname)
        if t == VNodeKind.verbatim:
          when vbtm:
            debug "dom: translating verbatim", false
            translateVbtm(el, q)
        else:
          if(el.hasAttr("alt") and el.isTranslatable("alt")) or
            (el.hasAttr("title") and el.isTranslatable("title")):
            translate(q.addr, el, srv)

Trên đây là vòng lặp chínhtranslateIter:

Bản dịch được áp dụng cho tất cả các nút văn bản và choalttitle thuộc tính.

proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service) =
  if q.isnil:
    warn "translate: queue can't be nil"
    return
  let (success, length) = setFromDB(q[].pair, el)
  if not success:
    if length > q[].bufsize:
      debug "Translating element singularly since it is big"
      elUpdate(q[], el, srv)
    else:
      if reachedBufSize(length, q[]):
        q[].push()
      q[].bucket.add(el)
      q[].sz += length

proc translate*[T](q: ptr[QueueXml | QueueDom], el: T, srv: service,
    finish: bool): Future[bool] {.async.} =
  if finish:
    if q.isnil:
      return true
    let (success, _) = setFromDB(q[].pair, el)
    if not success:
      addJob(@[el], q[], el.getText)
      debug "translate: waiting for pair: {q[].pair}"
      await doTrans()
  return true

proc translate*(q: ptr[QueueXml | QueueDom], srv: service,
    finish: bool): Future[bool] {.async.} =
  if finish and q[].sz > 0:
    q[].push()
    await doTrans()
    saveToDB(force = true)
  return true

Bởi vì chúng tôi phải dịch riêng từng nút văn bản (nếu không, chúng tôi không thể kết xuất lại html) nên mỗi bản dịch nút là một công việc riêng biệt. Vì các công việc có thể truy vấn các dịch vụ dịch thuật của mạng nên chúng phải được thực hiện không đồng bộ. Chúng tôi thực hiện tách và hợp nhất các truy vấn dịch thuật để dự phòng các lệnh gọi api, nhưng nội bộ của công cụ dịch thuật không quan trọng để biết. Điều duy nhất cần lưu ý là ban đầu tôi đang sử dụng một trình bao bọc trăn (mà tôi vẫn sử dụng để dịch nội dung cóp nhặt) vì trình bao bọc tự quản lý cho apis bên ngoài là một điều khó khăn, nhưng sau đó chuyển sang dịch vụ dịch google và yandex tự gói trong nim, vì python trở thành một nút cổ chai đáng kể khi xử lý nhiều bản dịch đồng thời.

[1]Trên thực tế, ban đầu trình tổng hợp nội dung được cho là chỉ tạo các tệp tĩnh chocaddy để phục vụ, nhưng vì số lượng trang cần tạo (là ma trận của n_lang(20) x amp(2) x trang), kết xuất chậm là tùy chọn tốt hơn.

số liệu thống kê

Các trang chủ đề và bài viết được theo dõi để biết số lần truy cập.

proc updateHits*(capts: UriCaptures) =
  let ak = join([capts.topic, capts.art])
  let tk = capts.topic
  var
    artCount: int32 = statsDB[ak]
    topicCount: int32 = statsDB[tk]
  artCount += 1
  topicCount += 1
  statsDB[ak] = artCount
  statsDB[tk] = topicCount

Chúng tôi sử dụng số lượt truy cập để dọn dẹp các trang có số lượng thấp theo định kỳ.

proc deleteLowTrafficArts*(topic: string): Future[void] {.gcsafe, async.} =
  let now = getTime()
  var
    pagenum: int
    pagesToReset: seq[int]
    pubTime: Time
    pubTimeTs: int
  var capts = mUriCaptures()
  capts.topic = topic
  for (art, _) in (await publishedArticles[string](topic, "")):
    withPyLock:
      if pyisnone(art):
        continue
      capts.art = pyget[string](art, "slug")
      pagenum = pyget(art, "page", 0)
    capts.page = pagenum.intToStr
    try:
      withPyLock:
        pubTimeTs = pyget(art, "pubTime", 0)
      pubTime = fromUnix(pubTimeTs)
    except:
      pubTime = default(Time)
    if pubTime == default(Time):
      if not (pagenum in pagesToReset):
        debug "tasks: resetting pubTime for page {pagenum}"
        pagesToReset.add pagenum
    # article is old enough
    elif inSeconds(now - pubTime) > cfg.CLEANUP_AGE:
      let hits = topic.getHits(capts.art)
      # article has low hit count
      if hits < cfg.CLEANUP_HITS:
        await deleteArt(capts)
  for n in pagesToReset:
    withPyLock:
      discard site[].update_pubtime(topic, n)

cơ sở dữ liệu

Chúng tôi sử dụnglibmdbx xuyên qua thư viện này . Có lẽ là quá mức cần thiết và việc sử dụng leveldb sẽ là đủ. Chúng tôi có một loạiLRUTrans trong đó ý tưởng ban đầu là thiết lập cơ sở dữ liệu dưới dạng bộ đệm LRU, nhưng nó chậm hơn đáng kể. Việc thực hiện có thể được tìm thấy ở đây

type
    CollectionNotNil = ptr Collection not nil
    LRUTransObj = object
        db: nimdbx.Database.Database not nil
        coll: CollectionNotNil
        zstd_c: ptr ZSTD_CCtx
        zstd_d: ptr ZSTD_DCtx
    LRUTrans* = ptr LRUTransObj

proc getImpl(t: LRUTrans, k: int64, throw: static bool): string =
    withLock(tLock):
        var o: seq[byte]
        t.coll.inSnapshot do (cs: CollectionSnapshot):
            # debug "nimdbx: looking for key {k}, {v}"
            o.add cs[k.asData].asByteSeq
        if len(o) > 0:
            result = cast[string](decompress(t.zstd_d, o))
            # debug "nimdbx: got key {k}, with {o.len} bytes"
        elif throw:
            raise newException(KeyError, "nimdbx: key not found")

proc getImpl[T: not int64](t: LRUTrans, k: T, throw: static bool): string =
    getImpl(t, hash(k).int64, throw)


proc `[]`*[T](t: LRUTrans, k: T): auto = t.getImpl(k, false)
proc `get`*[K](t: LRUTrans, k: K): auto = t.getImpl(k, true)

proc `[]=`*(t: LRUTrans, k: int64, v: string) {.gcsafe.} =
    var o: seq[byte]
    if likely(v.len != 0):
      o = compress(t.zstd_c, v, cfg.ZSTD_COMPRESSION_LEVEL)
    withLock(tLock):
        logall "nimdbx: saving key {k}"
        t.coll.inTransaction do (ct: CollectionTransaction):
            {.cast(gcsafe).}:
                ct[k] = o
            ct.commit()
        logall "nimdbx: commited key {k}"

proc `[]=`*[K: not int64](t: LRUTrans, k: K, v: string) = t[hash(k).int64] = v

Loại này được sử dụng cho bốn cơ sở dữ liệu riêng biệt:

Loại cơ sở dữ liệu được triển khai với getters và setters sau đó thực hiện tự động khử/nén khi đọc/ghi. Vì lý do này, nó không nên được sử dụng cho hình ảnh...nhưng than ôi... Ngoài ra còn có một loạt các bộ đệm vi mô nhỏ:

Chúng được thực hiện như lru bộ đệm[2] , chính xác hơn là bộ đệm lru "bị khóa", trong đó mọi thao tác lấy và đặt được bao quanh một khóa (luồng). Các khóa này không thể gây ra tình trạng dừng với thời gian chạy không đồng bộ vì khóa được mua và giải phóng mà không có bất kỳ câu lệnh lợi nhuận nào, vì vậy chúng là nguyên tử theo nghĩa đó, tuy nhiên chúng vẫn hữu ích vì chúng tôi sử dụng các luồng cho các tác vụ khác nhau.

[2]Tuy nhiên nim hầm có cách triển khai đơn giản hơn cho lru cache mà tôi đã sử dụng nếu được tìm thấy sớm hơn.

công việc nền

Một vài tác vụ mà chúng tôi sử dụng đang ngốn CPU, vì vậy chúng tôi sử dụng một luồng khác cho chúng:

Hai luồng nữa được sử dụng để cập nhật danh sách tệp nội dung và quảng cáo, mặc dù không đói cpu nhưng cần có một luồng để tránh tình trạng dừng do trình xem tệp gây ra.

Chúng tôi cũng có các tác vụ chạy dài không đồng bộ cho:

Các công việc yêu cầu Lsh, hình ảnh, dịch thuật và http được xử lý bằng cách sử dụng thiết lập của nhà sản xuất/người tiêu dùng. Ngoại trừ việc chúng tôi không sử dụng các kênh, vì các kênh chặn và chúng tôi không có triển khai không đồng bộ của chúng cũng là luồng an toàn. Chúng tôi đã sử dụng triển khai không đồng bộ của cái này[1] . Và một bảng không đồng bộ, giống như một xe buýt sự kiện

type
  AsyncTableObj[K, V] = object
    lock: ThreadLock
    waiters: Table[K, seq[ptr Future[V]]]
    table: Table[K, V]
  AsyncTable*[K, V] = ptr AsyncTableObj[K, V]

proc pop*[K, V](t: AsyncTable[K, V], k: K): Future[V] {.async.} =
  var popped = false
  withLock(t.lock):
    if k in t.table:
      popped = t.table.pop(k, result)
  if not popped:
    if k notin t.waiters:
      t.waiters[k] = newSeq[ptr Future[V]]()
    var fut = newFuture[V]("AsyncTable.pop")
    t.waiters[k].add fut.addr
    result = await fut

proc put*[K, V](t: AsyncTable[K, V], k: K, v: V) {.async.} =
  withLock(t.lock):
    if k in t.waiters:
      var ws: seq[ptr Future[V]]
      doassert t.waiters.pop(k, ws)
      while ws.len > 0:
        let w = ws.pop()
        if not w.isnil and not w[].isnil and not w[].finished:
          w[].complete(v)
    else:
      t.table[k] = v

Máy chủ nim cũng xử lý ba tác vụ không đồng bộ:

type
  TaskKind = enum pub, cleanup, mem

proc scheduleTasks(): TaskTable =
  template addTask(t) =
    let fut = (selectTask t)()
    result[t] = fut
  # Publishes new articles for one topic every x seconds
  addTask pub
  # cleanup task for deleting low traffic articles
  addTask cleanup
  # quit when max memory usage reached
  addTask mem

Nhiệm vụ giám sát việc sử dụng mem là rất tốt, để tránh các sự cố OOM giữa quy trình được chứa trong vùng chứa và docker, bởi vì docker (hoặc kernel) không tắt quy trình ngay lập tức và trong khoảng thời gian này, máy chủ có thể không phản hồi, vì vậy tốt hơn là khởi động lại thủ công ngay lập tức.

[1]mặc dù gói một kênh đơn giản trong các thói quen không đồng bộ có lẽ tốt hơn ... than ôi

Hình ảnh

chúng tôi tận dụng luồng hình ảnh để thay đổi kích thước và lưu trữ hình ảnh cục bộ. Các ràng buộc rất đơn giản, nhưng quá trình này có một chút liên quan. VớigetImg chúng tôi tìm nạp dữ liệu hình ảnh từ url từ xa:

proc getImg*(src: string, kind: Source): Future[string] {.async.} =
  return case kind:
    of urlsrc:
      (await get(src.parseUri, decode = false, proxied = false)).body
    elif fileExists(src):
      await readFileAsync(src)
    else:
      ""

Sau đó, chúng ta phải thêm nó vào bối cảnh luồng hình ảnh:

proc addImg*(img: string): bool =
  ## a lock should be held here throughout the `processImg` call.
  if img == "": return false
  reset(ctx)
  doassert ctx.check
  let a = imageflow_context_add_input_buffer(
    ctx.p,
    inputIoId,
    # NOTE: The image is held in cache, but it might be collected
    cast[ptr uint8](img[0].unsafeAddr),
    img.len.csize_t,
    imageflow_lifetime_lifetime_outlives_context)
  if not a:
    doassert ctx.check
    cmdStr["decode"] = %inputIoId
  return true

Nếu không thể thêm hình ảnh, điều đó có nghĩa là luồng hình ảnh không nhận dạng được dữ liệu là hình ảnh hợp lệ. Sau khi chúng tôi đã gửi dữ liệu, chúng tôi phải gửi một truy vấn đến ngữ cảnh, sau đó đọc phản hồi và nhận đầu ra:

proc doProcessImg(input: string, mtd = execMethod): (string, string) =
  setCmd(input)
  let c = $cmd
  # debug "{hash(c)} - {c}"
  let json_res = imageflow_context_send_json(
      ctx.p,
      mtd,
      cast[ptr uint8](c[0].unsafeAddr),
      c.len.csize_t
    )
  discard imageflow_json_response_read(ctx.p, json_res,
                                       status.addr,
                                       resPtr,
                                       resLen)
  defer: doassert imageflow_json_response_destroy(ctx.p, json_res)

  var mime: string
  if status != 200:
    let msg = resPtr[].toString(resLen[].int)
    debug "imageflow: conversion failed {msg}"
    doassert ctx.check
  else:
    mime = getMime()
  discard imageflow_context_get_output_buffer_by_id(
      ctx.p,
      outputIoId,
      outputBuffer,
      outputBufferLen)
  doassert ctx.check
  result = (outputBuffer[].toString(outputBufferLen[].int), mime)

Chúng tôi nhận được loại mime từ phản hồi, loại này sẽ được chuyển tiếp trong phản hồi của máy chủ web. Từ phía máy chủ, quá trình dịch từ đường dẫn url sang luồng hình ảnh được xử lý như sau:

proc processImgData(q: ptr ImgQuery) {.async.} =
  # push img to imageflow context
  initImageFlow() # NOTE: this initializes thread vars
  var acquired, submitted: bool
  let data = (await q.url.rawImg)
  defer:
    if acquired: imgLock[].release
    if not submitted:
      imgOut[q] = true
  if data.len > 0:
    try:
      await imgLock[].acquire
      acquired = true
      if addImg(data):
        let query = fmt"width={q.width}&height={q.height}&mode=max&format=webp"
        logall "ifl server: serving image hash: {hash(await q.url.rawImg)}, size: {q.width}x{q.height}"
        # process and send back
        (q.processed.data, q.processed.mime) = processImg(query)
        imgOut[q] = true
        submitted = true
    except Exception:
      discard

Url hình ảnh được gửi dưới dạng tham số, ở dạng nén zstd. Việc nén rút ngắn các url (hầu hết thời gian). Đây cũng là cách tôi tìm thấy một lỗi trong google chrome, lỗi không thể xử lý các url trong đó truy vấn có dữ liệu nén được mã hóa url. Thay vào đó, Firefox vẫn ổn.

LD-JSON

Chúng tôi thêm vào mỗi trang web tập lệnh ldjson.

proc jwebpage(id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
            image = "", entity = "Article", status = "Published", lang = "english", mentions: seq[
            string] = (@[]), access_mode = (@["textual", "visual"]), access_sufficient: seq[
            string] = @[], access_summary = "", created = "", published = "",
            props = default(JsonNode)): JsonNode =
    let
        d_mtime = coerce(mtime, "")
        s_created = created.toIsoDate
        description = coerce(description, to = title)
        prd = (v: seq[string]) => v.len == 0

    let data = %*{
        "@context": "https://schema.org",
        "@type": "https://schema.org/WebPage",
        "@id": id,
        "url": url,
        "lastReviewed": coerce(mtime, ""),
        "mainEntityOfPage": {
            "@type": entity,
            "@id": url
        },
        "mainContentOfPage":
        {
            "@type": "WebPageElement", "cssSelector": selector},
        "accessMode": access_mode,
        "accessModeSufficient": {
            "@type": "itemList",
            "itemListElement": coercf(access_sufficient, prd, to = access_mode),
        },
        "creativeWorkStatus": status,
        # NOTE: datePublished should always be provided
        "datePublished": ensure_time(d_mtime.toIsoDate, s_created),
        "dateModified": d_mtime,
        "dateCreated": coerce(s_created, to = d_mtime),
        "name": coerce(name, to = title),
        "description": coerce(description, ""),
        "keywords": coerce(keywords, to = (@[]))
    }
    setArgs data, %*{"inLanguage": lang, "accessibilitySummary": access_summary,
                    "headline": coerce(headline, to = description), "image": image,
                    "mentions": mentions}
    setProps
    data

Và đối với các trang đã dịch:

proc translation*(src_url, trg_url, lang, title, mtime, selector, description: auto, keywords: seq[string],
                     image = "", headline = "", props = default(JsonNode),
                     translator_name = "Google", translator_url = "https://translate.google.com/"): auto =
    ## file path must be relative to the project directory, assumes the published website is under '__site/'
    # id, title, url, mtime, selector, description: auto, keywords: seq[string], name = "", headline = "",
    let data = jwebpage(id = trg_url, title, url = trg_url, mtime, selector, description,
                            keywords = keywords, image = image, headline = headline, lang = lang, props = props)
    data["translator"] = %*{"@type": "https://schema.org/Organization",
                             "name": translator_name,
                             "url": translator_url}
    data["translationOfWork"] = %*{"@id": src_url}
    data

Đồ thị mở

Tương tự như ldjson, chúng tôi cũng cung cấp các thẻ meta opengraph:

proc opgBasic(title, tp, url, image: string, prefix = ""): seq[XmlNode] =
  if prefix != "":
    result.add metaTag(fmt"{prefix}:title", title)
    result.add metaTag(fmt"{prefix}:type", tp)
    result.add metaTag(fmt"{prefix}:url", url)
    result.add metaTag(fmt"{prefix}:image", image)
  else:
    result.add metaTag("title", image)
    result.add metaTag("type", image)
    result.add metaTag("url", image)
    result.add metaTag("image", image)

proc opgTags(title, tp, url,
             image: string,
             description = "",
             siteName = "",
             locale = "",
             audio = "",
             video = "",
             determiner = "",
             prefix = ""): seq[XmlNode] {.gcsafe.} =
  ## Generates an HTML String containing opengraph meta result for one item.
  var result = opgBasic(title, tp, url, image, prefix)
  result.add opgOptional(description, siteName, locale, audio, video, determiner)
  return result

proc opgPage*(a: Article): seq[XmlNode] =
  let locale = static(DEFAULT_LOCALE)
  let
    tp = static("article")
    url = getArticleUrl(a)
    siteName = static(WEBSITE_TITLE)
  result = opgTags(a.title, tp, url, a.imageUrl, a.desc, siteName, locale, prefix = "article")
  for t in a.tags:
    result.add metaTag("article:tag", t)
  result.add metaTag("article:author", a.author)
  result.add metaTag("article:published_time", $a.pubTime)
  result.add metaTag("article:section", a.desc)
  # result.add metaTag("article:modified_time", a.pubTime)
  # result.add metaTag("article:expiration_time", a.pubTime)
  result.add twitterMeta("card", "summary")
  result.add twitterMeta("creator", twitterUrl[])

Các mẫu và macro Nim có ích khi xử lý tất cả mã nặng soạn sẵn này.

Yêu cầu http phía máy chủ

Có một tác vụ khác, xử lý tất cả các yêu cầu http (để tìm nạp hình ảnh, tập lệnh, v.v.) từ phía máy chủ web. Chúng tôi sử dụng chronos httpclient:

const proxiedFlags = {NoVerifyHost, NoVerifyServerName, NewConnectionAlways}
const sessionFlags = {NoVerifyHost, NoVerifyServerName, NoInet6Resolution}
proc requestTask(q: sink ptr Request) {.async.} =
  var trial = 0
  var
    sess: HttpSessionRef
    req: HttpClientRequestRef
    resp: HttpClientResponseRef
    cleanup: seq[Future[void]]
  while trial < q[].retries:
    try:
      trial.inc
      sess = new(HttpSessionRef,
                proxyTimeout = 10.seconds.div(3),
                headersTimeout = 10.seconds.div(2),
                connectTimeout = 10.seconds,
                proxy = if q[].proxied: selectProxy(trial) else: "",
                flags = if q[].proxied: proxiedFlags else: sessionFlags
      )
      req = new(HttpClientRequestRef,
                sess,
                sess.getAddress(q[].url).get,
                q[].meth,
                headers = q[].headers.toHeaderTuple,
                body = q[].body.tobytes,
        )
      resp = await req.fetch(followRedirects = q[].redir, raw = true)
      checkNil(resp):
        defer:
          cleanup.add resp.closeWait()
          resp = nil
        q.response.code = httpcore.HttpCode(resp.status)
        checkNil(resp.connection):
          q.response.body = bytesToString (await resp.getBodyBytes)
          q.response.headers = newHttpHeaders(cast[seq[(string, string)]](
              resp.headers.toList))
        break
    except:
      cdebug():
        logexc()
        debug "cronhttp: request failed"
    finally:
      if not req.isnil:
        cleanup.add req.closeWait()
      if not resp.isnil:
        cleanup.add resp.closeWait()
      if not sess.isnil:
        cleanup.add sess.closeWait()
  httpOut[q] = true
  await allFutures(cleanup)

tôi đã phải thêm hỗ trợ cho proxy https và vớ5 đến httpclient để có thể sử dụng các bản dịch một cách hiệu quả.

cấu hình

Bạn có thể đã nhận thấy các biến được viết hoa trong toàn bộ mã. Tất cả đều là các biến cấu hình, được xác định trong một tệp, có thể được tùy chỉnh cho mỗi trang web.

const
  BASE_URL* = Uri()
  SITE_PATH* = PROJECT_PATH / "site"
  SITE_ASSETS_PATH* = BASE_URL / "assets" / WEBSITE_NAME
  SITE_ASSETS_DIR* = SITE_PATH / "assets" / WEBSITE_NAME
  DATA_PATH* = PROJECT_PATH / "data"
  DATA_ASSETS_PATH* = DATA_PATH / "assets" / WEBSITE_NAME
  DATA_ADS_PATH* = DATA_PATH / "ads" / WEBSITE_NAME
  ASSETS_PATH* = PROJECT_PATH / "src" / "assets"
  DEFAULT_IMAGE* = ASSETS_PATH / "empty.png"
  DEFAULT_IMAGE_MIME* = "image/png"
  CSS_BUN_URL* = $(SITE_ASSETS_PATH / "bundle.css")
  CSS_CRIT_PATH* = SITE_ASSETS_DIR / "bundle-crit.css"
  JS_REL_URL* = $(SITE_ASSETS_PATH / "bundle.js")
  LOGO_PATH* = BASE_URL / "assets" / "logo" / WEBSITE_NAME
  LOGO_URL* = $(LOGO_PATH / "logo.svg")
  LOGO_SMALL_URL* = $(LOGO_PATH / "logo-small.svg")
  LOGO_ICON_URL* = $(LOGO_PATH / "logo-icon.svg")
  LOGO_DARK_URL* = $(LOGO_PATH / "logo-dark.svg")
  LOGO_DARK_SMALL_URL* = $(LOGO_PATH / "logo-small-dark.svg")
  LOGO_DARK_ICON_URL* = $(LOGO_PATH / "logo-icon-dark.svg")
  FAVICON_PNG_URL* = $(LOGO_PATH / "logo-icon.png")
  FAVICON_SVG_URL* = $(LOGO_PATH / "logo-icon.svg")
  APPLE_PNG180_URL* = $(LOGO_PATH / "apple-touch-icon.png")
  MAX_DIR_FILES* = 10
# ...

Phần kết luận

Có rất nhiều thứ mà tôi chưa đề cập đến, vì ma quỷ nằm trong chi tiết... tuy nhiên đây là chuyến tham quan sơ bộ toàn bộ cơ sở mã có giá trị:

Tôi sẽ làm gì khác đi?

Thẻ bài: