Как буферизовать и обрабатывать в пакетном режиме выходной файл? - PullRequest
1 голос
/ 07 января 2020

Мне нужно отслеживать файл и отправлять записанное в него веб-сервису. Я пытаюсь найти чистое и простое решение с помощью bash сценариев, например:

#!/bin/bash

# listen for changes on file specified as first argument
tail -F "$1" | while read LINE
do
  curl http://service.com/endpoint --data "${LINE}"
done

Это работает отлично, как в ... каждая добавляемая строка будет POST'ed к http://service.com/endpoint. Тем не менее, мне не очень нравится тот факт, что, если за короткое время будет добавлено много строк, у меня будет столько же HTTP-запросов и, возможно, будет перегружен сервис.

Существует ли разумный способ буферизовать операции? Я могу думать о чем-то вроде:

buffer = EMPTY
while LINES are read:
  add LINE to buffer
  if buffer has more than X LINES
    send POST
  fi
done

Но в приведенном выше решении, если в час публикуется одна строка, я буду получать обновления каждые X часов, что недопустимо. Другим аналогичным решением было бы «время» в то время как l oop: if X seconds have passed then send buffer, otherwise wait .., но последняя строка потока может удерживаться неопределенно долго, так как while l oop запускается только тогда, когда новая строка добавляется в файл.

Цель состоит в том, чтобы сделать это с минимальными сценариями bash и без использования второго процесса . Под вторым процессом я подразумеваю: process 1 gets the output from tail -f and stores it и process 2 periodically checks what is stored and sends a POST if more than x seconds are elapsed?

Мне любопытно, возможно ли это с помощью какой-нибудь хитрой уловки?

Спасибо!

Ответы [ 2 ]

2 голосов
/ 07 января 2020

Буквально помещая ваш псевдокод в код:

# add stdbuf -oL if you care
tail -F "$1" | {
    # buffer = EMPTY
    buffer=
    # while LINES are read:
    while IFS= read -r line; do
      # add LINE to buffer
      buffer+="$line"$'\n'
      # if buffer has more than X LINES
      # TODO: cache the count of lines in a variable to save cpu
      if [ $(wc -l <<<"$buffer") -gt "$x_lines" ]; then
          # send POST
          # TODO: remove additional newline on the end of buffer, if needed
          curl http://service.com/endpoint --data "${buffer}"
          buffer=
      fi
    done
}

Удаление новой строки в конце буфера или, например, буферизация количества строк в отдельном счетчике для сохранения процессора оставлена ​​для других.

Примечания:

  • Прописные переменные по соглашению зарезервированы для глобальных экспортируемых переменных.
  • while read LINE удалит начальные и конечные пробелы из строки. Используйте while IFS= read -r line, чтобы прочитать всю строку. Больше информации на bashfaq о том, как читать файл построчно
  • С одной строкой, я думаю, вы можете просто использовать xargs как tail -F "$1" | xargs -d$'\n' -n1 curl http://service.com/endpoint --data

Для буфер со временем, тайм-аут чтения - либо с расширением bash, напр. read -t 0.1 или путем тайм-аута всего чтения timeout 1 cat.

Чтобы ограничить в обоих отношениях количество строк и время ожидания, я однажды написал сценарий с неправильным названием ratelimit. sh (плохо назван, потому что он не ограничивает скорость ...), это именно то, что нужно. Он читает строки, и, если число строк или время ожидания достигнуто, он очищает свой буфер дополнительным выходным разделителем. Я считаю, что это должно быть использовано как tail -F "$1" | ratelimit.sh --timeout=0.5 --lines=5 | while IFS= read -r -d $'\x02' buffer; do curl ... --data "$buffer"; done. Это примерно работает так:

# Written by Kamil Cukrowski (C) 2020
# Licensed jointly under MIT and Beerware license
# config
maxtimeoutns=$((2 * 1000 * 1000 * 1000))
maxlines=5 
input_separator=$'\n'
output_separator=$'\x02'

# the script
timeout_arg=()
while true; do
    chunk=""
    lines=0
    start=$(date +%s%N)
    stop=$((start + maxtimeoutns))

    while true; do

        if [ "$maxtimeoutns" != 0 ]; then
            now=$(date +%s%N)
            if (( now >= stop )); then
                break
            fi
            timeout=$(( stop - now ))
            timeout=$(awk -va=$timeout -vb=1000000000 '{print "%f", a/b}' <<<"")
            timeout_arg=(-t "$timeout")
        fi


        IFS= read -rd "$input_separator" "${timeout_arg[@]}" line && ret=$? || ret=$?

        if (( ret == 0 )); then

            # read succeded
            chunk+=$line$'\n'

            if (( maxlines != 0 )); then
                lines=$((lines + 1))
                if (( lines >= maxlines )); then
                    break
                fi
            fi

        elif (( ret > 128 )); then
            # read timeouted
            break;
        fi
    done

    if (( ${#chunk} != 0 )); then
        printf "%s%s" "$chunk" "$output_separator"
    fi

done
0 голосов
/ 07 января 2020

Благодаря ответу KamilCuk мне удалось добиться того, что я хотел, довольно простым способом, сочетая максимальное количество линий и тайм-ауты. Уловка состояла в том, чтобы обнаружить, что трубопровод не обязательно работает по линиям, как я думал, что он работал ... просто я!

Просто для справки на будущее, это мое решение, которое очень конкретно c и упрощено до кость:

#!/bin/bash
# sends updates to $1 via curl every 15 seconds or every 100 lines
tail -F "$1" | while true; do

    chunk=""
    stop=$((`date +%s` + 15))
    maxlines=100

    while true; do

        if (( `date +%s` >= stop )); then break; fi

        IFS= read -r -t 15 line && ret=$? || ret=$?         
        if (( ret == 0 )); then

                chunk+=$line$'\n'
                maxlines=$((maxlines - 1))
                if (( maxlines == 0 )); then break; fi

        elif (( ret > 128 )); then break; fi

    done

    if (( ${#chunk} != 0 )); then
        curl http://service.com --data "$chunk";
    fi

done
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...