Цикл Batch For не обновляет файл, из которого извлекается - PullRequest
1 голос
/ 26 января 2012

Итак, у меня есть цикл for, который выполняет итерацию хранимой процедуры SQL для каждой строки в файле queue.txt, теперь, когда все прекрасно работает, однако, ДОЛЖЕН сделать вывод, что если он повторяется и в строку добавляется другая строка внизу файла, который он использует в качестве критерия итерации, тогда он просто игнорирует его.

Что у меня есть это:

@echo off
cd "%UserProfile%\Desktop\Scripting\"
echo words > busy.txt

FOR /f "delims=" %%a in ('type queue.txt') DO (
IF NOT EXIST reset.sql (

::Create SQL command
echo USE dbname> reset.sql
echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'>> reset.sql
echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'>> reset.sql
echo #################### %date% - %time% ####################################################>> log.txt
echo Reinitialising '%%a'>> log.txt
sqlcmd -i "reset.sql">> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

type queue.txt | findstr /v %%a> new.txt
type new.txt> queue.txt
echo New list of laptops waiting:>> log.txt
type queue.txt>> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

if exist reset.sql del /f /q reset.sql

) 
)

if exist busy.txt del /f /q busy.txt
if exist queue.txt del /f /q queue.txt
if exist new.txt del /f /q new.txt

Итак, он извлекает файл queue.txt и выполняет итерацию для каждого из них, теперь скажем, что он начинается с 2 строк в файле, это здорово, он запускает процедуры для них.

Теперь, скажем, я добавляю еще одну строку к queue.txt. Пока цикл выполняется, он просто игнорирует эту строку, поэтому похоже, что for не обновляется из файла на каждой итерации, которую он просто импортирует один раз.

Один из способов решить эту проблему - подсчитать количество строк на первой итерации цикла, а затем проверить его в конце каждой итерации в соответствии с тем значением, которое, по его мнению, должно быть, и если оно больше он ожидает возврата к циклу for (используя goto или что-то подобное), но gotos не работает в логических выражениях.

Посоветуйте кому-нибудь пожалуйста?

Ответы [ 4 ]

3 голосов
/ 28 января 2012

@ Myles Grey - Ваше решение имеет некоторые проблемы.

Сначала незначительные проблемы:

1) После каждой итерации цикла очереди вы воссоздаете очередь как исходную очередь за вычетомлиния, над которой вы сейчас работаете (надеюсь, подробнее об этом позже).После воссоздания очереди вы добавляете ее в свой журнал.Это сработает, но кажется очень неэффективным и может сделать журнал массивным и непривлекательным.Предположим, у вас есть очередь с 10 000 строк.К тому времени, как вы обработали свою очередь, вы записали в свой журнал 99 989 998 строк очереди, включая 49 994 999 строк очереди!Это займет много времени для обработки, даже без фактического выполнения вашей работы.

2) Вы воссоздаете очередь, используя FINDSTR, сохраняя все строки, которые не соответствуют вашему текущему идентификатору.Но это также удалит последующие строки, если они совпадут с вашим текущим идентификатором.Это не может быть проблемой.Но вы делаете совпадение подстроки.Ваш FINDSTR также удалит последующие строки, которые содержат ваш текущий идентификатор в любом месте внутри него.Я понятия не имею, как выглядят ваши идентификаторы.Но если ваш текущий идентификатор равен 123, то все следующие идентификаторы будут ошибочно удалены - 31236, 12365 и т. Д. Это является потенциально разрушающей проблемой.Я говорю, что это возможно, потому что цикл FOR уже буферизовал очередь, поэтому ему все равно - если вы не прервете цикл, потому что новая работа была добавлена ​​в файл late.txt - тогда вы фактически пропустите эти недостающие идентификаторы!Это можно исправить, добавив параметр / X в FINDSTR.По крайней мере, тогда вы будете пропускать только истинные дубликаты.

Теперь основные проблемы - все это связано с тем фактом, что только у одного процесса может быть открыт файл для любой операции записи (или удаления).

3) Хотя цикл FOR / F не записывает данные в файл, он предназначен для сбоя, если файл активно записывается другим процессом.Поэтому, если ваш цикл FOR пытается прочитать очередь, пока к ней добавляется другой процесс, ваш сценарий обработки очереди завершится неудачно.У вас есть проверка файла busy.txt, но ваш писатель очереди, возможно, уже начал писать до того, как был создан файл busy.txt.Операция записи может занять некоторое время, особенно если добавляется много строк.Во время написания строк ваш обработчик очереди может запуститься, и тогда у вас возникнет коллизия и сбой.

4) Обработчик очереди добавляет в свою очередь файл late.txt, а затем удаляет late.txt.Но между добавлением и удалением существует момент времени, когда средство записи очереди может добавить дополнительную строку к late.txt.Эта поздняя поступающая строка будет удалена без обработки!

5) Другая возможность - автор может попытаться записать в файл late.txt, пока он находится в процессе удаления процессором очереди.Запись не удастся, и снова в вашей очереди будет отсутствовать работа.

6) Еще одна возможность - ваша очередь может попытаться удалить late.txt, пока к ней добавляется средство записи очереди.Удаление не удастся, и вы получите дубликаты в своей очереди в следующий раз, когда обработчик очереди добавит late.txt к queue.txt.

В итоге проблемы параллелизма могут привести к отсутствию работы в вашей очереди., а также дублируй работу в своей очереди.Когда у вас есть несколько процессов, вносящих изменения в файл одновременно, вы ДОЛЖНЫ установить какой-то механизм блокировки для сериализации событий.

Вы уже используете базу данных SqlServer.Самое логичное, что нужно сделать - это переместить свою очередь из файловой системы в базу данных.Реляционные базы данных создаются с нуля, чтобы иметь дело с параллелизмом.

При этом не сложно использовать файл в качестве очереди в пакете Windows, если вы используете стратегию блокировки.Вы должны убедиться, что и ваш обработчик очереди, и ваши авторы очередей следуют одинаковой стратегии блокировки.

Ниже приведено файловое решение.Я собираюсь предположить, что у вас есть только один обработчик очереди и, возможно, несколько авторов очереди.С дополнительной работой вы можете адаптировать решение очереди файлов для поддержки нескольких процессоров очереди.Но несколько процессоров очереди, вероятно, проще реализовать, используя очередь на основе папок, которую я описал в конце моего первого ответа .

Вместо того, чтобы пишущие очереди записывали в файл queue.txt илив конце.txt проще обработать процессором очереди переименование существующей очереди и обработать ее до конца, в то время как средства записи очереди всегда записывают в queue.txt.

Это решение записывает текущий статус в файл status.txt.файл.Вы можете отслеживать состояние обработчика очереди, введя TYPE STATUS.TXT из командного окна.

Я выполняю несколько отложенных расширений, чтобы защитить от повреждения из-за ! в ваших данных.Если вы знаете, что ! никогда не появится, то вы можете просто переместить SETLOCAL EnableDelayedExpansion наверх и отказаться от переключения.

Еще одна оптимизация - быстрее перенаправить вывод только один раз для группы оператороввместо того, чтобы открывать и закрывать файл для каждого оператора.

Этот код полностью не проверен, поэтому могут быть некоторые глупые ошибки.Но концепции здоровы.Надеюсь, вы поняли.

queueProcessor.bat

@echo off
setlocal disableDelayedExpansion
cd "%UserProfile%\Desktop\Scripting\"

:rerun

::Safely get a copy of the current queue, exit if none or error
call :getQueue || exit /b

::Get the number of lines in the queue to be used in status updates
for /f %%n in ('find /v "" ^<inProcess.txt') do set /a "record=0, recordCount=%%n"

::Main processing loop
for /f "delims=" %%a in (inProcess.txt) do (

  rem :: Update the status. Need delayed expansion to access the current record number.
  rem :: Need to toggle delayed expansion in case your data contains !
  setlocal enableDelayedExpansion
  set /a "record+=1"
  > status.txt echo processing !record! out of %recordCount%
  endlocal

  rem :: Create SQL command
  > reset.sql (
    echo USE dbname
    echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'
    echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'
  )

  rem :: Log this action and execute the SQL command
  >> log.txt (
    echo #################### %date% - %time% ####################################################
    echo Reinitialising '%%a'
    sqlcmd -i "reset.sql"
    echo.
    echo ####################################################################################################
    echo.
  )
)

::Clean up
delete inProcess.txt
delete status.txt

::Look for more work
goto :rerun

:getQueue
2>nul (
  >queue.lock (
    if not exist queue.txt exit /b 1
    if exist inProcess.txt (
      echo ERROR: Only one queue processor allowed at a time
      exit /b 2
    )
    rename queue.txt inProcess.txt
  )
)||goto :getQueue
exit /b 0

queueWriter.bat

::Whatever your code is
::At some point you want to append a VALUE to the queue in a safe way
call :appendQueue VALUE
::continue on until done
exit /b

:appendQueue
2>nul (
  >queue.lock (
    >>queue.txt echo %*
  )
)||goto :appendQueue

Объяснение кода блокировки:

:retry
::First redirect any error messages that occur within the outer block to nul
2>nul (

  rem ::Next redirect all stdout within the inner block to queue.lock
  rem ::No output will actually go there. But the file will be created
  rem ::and this process will have a lock on the file until the inner
  rem ::block completes. Any other process that tries to write to this
  rem ::file will fail. If a different process already has queue.lock 
  rem ::locked, then this process will fail to get the lock and the inner
  rem ::block will not execute. Any error message will go to nul.
  >queue.lock (

    rem ::you can now safely manipulate your queue because you have an
    rem ::exclusive lock.
    >>queue.txt echo data 

    rem ::If some command within the inner block can fail, then you must
    rem ::clear the error at the end of the inner block. Otherwise this
    rem ::routine can get stuck in an endless loop. You might want to 
    rem ::add this to my code - it clears any error.
    verify >nul

  ) && (

    rem ::I've never done this before, but if the inner block succeeded,
    rem ::then I think you can attempt to delete queue.lock at this point.
    rem ::If the del succeeds then you know that no process has a lock
    rem ::at this point. This could be useful if you are trying to monitor
    rem ::the processes. If the del fails then that means some other process
    rem ::has already grabbed the lock. You need to clear the error at
    rem ::this point to prevent the endless loop
    del queue.lock || verify >nul

  )

) || goto :retry
:: If the inner block failed to get the lock, then the conditional GOTO
:: activates and it loops back to try again. It continues to loop until
:: the lock succeeds. Note - the :retry label must be above the outer-
:: most block.

Если у вас есть уникальный идентификатор процесса, вы можете записать его в queue.lock во внутреннем блоке.Затем вы можете ввести queue.lock из другого окна, чтобы узнать, какой процесс в данный момент (или совсем недавно) имел блокировку.Это должно быть проблемой, только если какой-то процесс зависает.

2 голосов
/ 26 января 2012

Вы абсолютно правы - цикл FOR / F ожидает завершения команды в предложении IN () и буферизирует результат перед обработкой 1-й строки.То же самое верно, если вы читаете из файла в предложении IN () вместо выполнения команды.

Предложенная вами стратегия подсчета количества строк в очереди до цикла FOR, а затем повторного подсчета послеЦикл FOR завершен, может просто сработать, если вы перестанете связываться с содержимым очереди в цикле FOR.Если итоговое значение больше исходного, вы можете ЗАПУСТИТЬ метку a: перед циклом FOR и пропустить исходное число строк в цикле FOR, чтобы обрабатывать только добавленные строки.Но у вас все равно будет проблема параллелизма, если процесс записывает в очередь, пока вы получаете счетчик строк, или если он добавляется в очередь после того, как вы получили окончательный счет, но перед удалением очереди.

Естьспособы сериализации событий внутри пакета при работе с несколькими процессами.Ключом к этому является использование того факта, что только один процесс может иметь файл, открытый для доступа для записи.

Код, подобный следующему, можно использовать для установления исключительной «блокировки».Пока каждый процесс использует одну и ту же логику, вы можете гарантировать исключительный контроль над одним или несколькими объектами файловой системы, пока не снимите блокировку, выйдя из блока кода.

:getLock
2>nul (
  >lockName.lock (
    rem ::You now have an exclusive lock while you remain in this block of code
    rem ::You can safely count the number of lines in a queue file,
    rem ::or append lines to the queue file at this time.
  )
)||goto :getLock

Я продемонстрировал, как это может работать на Re: параллельный процесс с пакетом .После нажатия на ссылку прокрутите вверх, чтобы увидеть исходный вопрос.Это похоже на вашу проблему.

Возможно, вы захотите использовать папку в качестве очереди, а не файла.Каждая единица работы может быть отдельным файлом в папке.Вы можете использовать блокировку для безопасного увеличения порядкового номера в файле, который будет использоваться при именовании каждой единицы работы.Вы можете гарантировать, что единица работы была полностью записана, подготовив ее в папке «preperation» и перемещая ее в папку «queue» только после ее завершения.Преимущество этой стратегии заключается в том, что каждая единица рабочего файла может быть перемещена в папку «inProcess» во время обработки, а затем она может быть удалена или перемещена в папку архива после завершения.Если обработка не удалась, вы можете восстановить, потому что файл все еще существует в папке «inProcess».Вы можете узнать, какие единицы работы являются нестабильными (мертвые в папке «inProcess»), а также какие единицы работы еще не обработаны вообще (те, которые все еще находятся в папке «очередь»).

1 голос
/ 28 января 2012

Вы задаете вопрос "если другая строка добавлена ​​ в конец файла ..."; однако ваш код не добавляет строку, а полностью заменяет все содержимое файла (хотя новое содержимое имеет только одну добавленную новую строку):

FOR /f "delims=" %%a in ('type queue.txt') DO (
   IF NOT EXIST reset.sql (

   . . .

   type queue.txt | findstr /v %%a> new.txt
   rem Next line REPLACES the entire queue.txt file!
   type new.txt> queue.txt
   echo New list of laptops waiting:>> log.txt

   . . .

   if exist reset.sql del /f /q reset.sql

   ) 
)

Вы можете изменить метод для обработки файла queue.txt, перенаправив его в подпрограмму, которая считывает его строки с помощью команды SET / P и цикла, собранного с GOTO. Таким образом, строки, которые добавляются в конец файла queue.txt внутри цикла чтения, будут сразу же прочитаны, когда процесс чтения достигнет их.

call :ProcessQueue < queue.txt >> queue.txt
goto :EOF


:ProcessQueue
   set line=
   rem Next command read a line from queue.txt file:
   set /P line=
   if not defined line goto endProcessQueue
   rem In following code use %line% instead of %%a
   IF NOT EXIST reset.sql (

   . . .

   type queue.txt | findstr /v %%a> new.txt
   rem Next command ADD new lines to queue.txt file:
   type new.txt
   echo New list of laptops waiting:>> log.txt

   . . .

   if exist reset.sql del /f /q reset.sql

   ) 
goto ProcessQueue
:endProcessQueue
exit /B

Конечно, если новые строки добавляются другими процессами , новые строки будут считываться и обрабатываться этим пакетным файлом автоматически.

Вы должны знать, что этот метод заканчивается на первой пустой строке в файле queue.txt; у него также есть некоторые ограничения в символах, которые он может обрабатывать.

РЕДАКТИРОВАТЬ : Это простой пример, показывающий, как работает этот метод:

set i=0
call :ProcessQueue < queue.txt >> queue.txt
goto :EOF

:ProcessQueue
   set line=
   set /P line=
   if not defined line goto endProcessQueue
   echo Line processed: %line% > CON
   set /A i=i+1
   if %i% == 1 echo First line added to queue.txt
   if %i% == 2 echo Second line added to queue.txt
goto ProcessQueue
:endProcessQueue
exit /B

Это файл queue.txt при вводе:

Original first line
Original second line
Original third line
Original fourth line

Это результат:

Line processed: Original first line
Line processed: Original second line
Line processed: Original third line
Line processed: Original fourth line
Line processed: First line added to queue.txt
Line processed: Second line added to queue.txt
0 голосов
/ 27 января 2012

Хорошо, поэтому моя проблема, которую я разработал, состояла в том, чтобы добавить дополнительный пакетный файл с именем co-ordinator.bat, который проверял, присутствовал ли busy.txt, если это было так, он бы добавил подключаемые устройства в файл late.txt в конце каждой итерации цикла процесс проверяет наличие late.txt, если он присутствует, то объединяет его с queue.txt и затем использует goto из цикла наверх, чтобы перезапустить -инициализировать цикл for.

Код как таковой:

@echo off
cd "%UserProfile%\Desktop\Scripting\"
echo words > busy.txt
:rerun

FOR /f "delims=" %%a in ('type queue.txt') DO (
IF NOT EXIST reset.sql (

::Create SQL command
echo USE dbname> reset.sql
echo EXEC dbo.sp_ResetSubscription @ClientName = '%%a'>> reset.sql
echo EXEC dbo.sp_RunClientSnapshot @ClientName = '%%a'>> reset.sql
echo #################### %date% - %time% ####################################################>> log.txt
echo Reinitialising '%%a'>> log.txt
sqlcmd -i "reset.sql">> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

type queue.txt | findstr /v %%a> new.txt
type new.txt> queue.txt
echo New list of laptops waiting:>> log.txt
type queue.txt>> log.txt
echo. >> log.txt
echo ####################################################################################################>> log.txt
echo. >> log.txt

if exist reset.sql del /f /q reset.sql
if exist late.txt (
type late.txt>> queue.txt
del /f /q late.txt
goto rerun
)
) 
)

if exist late.txt del /f /q late.txt
if exist busy.txt del /f /q busy.txt
if exist queue.txt del /f /q queue.txt
if exist new.txt del /f /q new.txt
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...