Batch For循环不会刷新它所从的文件

时间:2012-01-26 16:36:40

标签: loops batch-file for-loop

所以我有一个for循环,它为文件queue.txt中的每一行执行SQL存储过程的迭代,现在一切都很好,但是如果它是迭代并且添加了另一行,那么DOESNT是什么在它使用的文件的底部作为迭代条件然后它只是忽略它。

我拥有的是:

@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行开始,这很好,它开始为它们运行程序。 / p>

现在,假设我向queue.txt添加了另一行。当循环正在运行时,它只是忽略该行,因此它看起来像for不会在每次迭代时从文件更新它只导入一次。

我想解决这个问题的一种方法是计算循环第一次迭代时的行数,然后在每次迭代结束时检查它与它认为的值应该是什么,如果它超过了然后它会回到for循环之上(使用goto或其他类似的东西)但是getos在逻辑表达式中不起作用。

有人建议吗?

4 个答案:

答案 0 :(得分:3)

@Myles Gray - 您的解决方案存在一些问题。

首先是小问题:

1)在队列循环的每次迭代之后,您将队列重新创建为原始队列减去您当前正在处理的行(您希望!稍后再详述)。重新创建队列后,将其附加到日志中。这将起作用,但它似乎非常低效,并且有可能使日志变得庞大而且不合适。假设您有一个包含10,000行的队列。当您处理完队列时,您将编写99,989,998个队列行,包括49,994,999个队列行到您的日志!即使没有真正开展工作,这也需要很长时间才能完成。

2)您使用FINDSTR重新创建队列,保留所有与您当前ID不匹配的行。但如果碰巧与您当前的ID相匹配,这也将删除后续行。这可能不是问题。但是你正在进行子串匹配。您的FINDSTR还将消除包含您当前ID的后续行。我不知道你的ID是什么样的。但是如果您当前的ID是123,那么以下所有ID都将被错误地剥离 - 31236,12365等。这是一个潜在的失败问题。我说这是潜在的,因为FOR循环已经缓冲了队列,所以它并不关心 - 除非你因为新工作被追加到late.txt文件而中止循环 - 然后你实际上会跳过那些丢失的ID !这可以通过向FINDSTR添加/ X选项来修复。至少那时你只会跳过真正的副本。

现在主要的问题 - 所有这些都源于这样一个事实:只有一个进程可以为任何类型的写(或删除)操作打开文件。

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批处理中将文件用作队列并不太困难。您必须确保队列处理器和队列编写器都遵循相同的锁定策略。

以下是基于文件的解决方案。我假设您只有一个队列处理器,可能还有多个队列编写器。通过其他工作,您可以调整文件队列解决方案以支持多个队列处理器。但是使用我在my first answer末尾描述的基于文件夹的队列可能更容易实现多个队列处理器。

不是让队列编写者写入queue.txt或late.txt,而是让队列处理器重命名现有队列并将其处理完成,而队列编写者总是写入queue.txt。 / p>

此解决方案将当前状态写入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.

如果您有唯一的进程ID,则可以将其写入内部块中的queue.lock。然后,您可以从另一个窗口键入queue.lock,以找出当前具有(或最近具有)锁定的进程。如果某个进程挂起,那应该只是一个问题。

答案 1 :(得分:2)

你是绝对正确的 - FOR / F循环等待IN()子句中的命令完成并在处理第一行之前缓冲结果。如果从IN()子句中的文件读取而不是执行命令,则也是如此。

你建议的策略是在FOR循环之前计算队列中的行数,然后在FOR循环完成之后重新计算,如果你停止在FOR循环中弄乱队列内容,那么这个策略就可以了。如果最终计数大于原始计数,则可以在FOR循环之前使用GOTO a:标签并跳过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: parallel process with batch如何发挥作用。按下链接后,向上滚动以查看原始问题。这似乎是一个非常类似的问题。

您可能需要考虑将文件夹用作队列而不是文件。每个工作单元都可以是文件夹中的自己的文件。您可以使用锁定安全地增加文件中的序列号,以用于命名每个工作单元。您可以通过在“preperation”文件夹中准备工作单元完全编写工作单元,并在完成后将其移动到“queue”文件夹。此策略的优点是每个工作单元文件都可以在处理过程中移动到“inProcess”文件夹,然后可以在完成后将其删除或移动到存档文件夹。如果处理失败,您可以恢复,因为该文件仍存在于“inProcess”文件夹中。您可以知道哪些工作单元不稳定(“inProcess”文件夹中的死工作单元),以及尚未处理的工作单元(仍处于“queue”文件夹中的工作单元)。

答案 2 :(得分:1)

您提出的问题是“如果另一行已添加到文件底部......”;但是,您的代码不会添加一行,但完全替换整个文件内容(尽管新内容只添加了一行):

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

   ) 
)

您可以通过将其重定向到通过SET / P命令读取其行和使用GOTO组装的循环的子程序来更改处理queue.txt文件的方法。这样,当读取进程到达时,将立即读取添加到读取循环内部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

答案 3 :(得分:0)

好吧所以我解决的问题就是添加一个名为co-ordinator.bat的额外批处理文件,检查是否存在busy.txt,如果是,那么它会将连接设备添加到文件late.txt在循环的每次迭代结束时,进程将检查是否存在late.txt,如果它存在则会将其与queue.txt合并,然后使用{{} 1}}从循环到顶部重新初始化for循环。

代码:

goto