2012-01-26 14 views
1

拉的文件所以我有一個for循環,爲文件queue.txt中的每一行做一個SQL存儲過程的迭代,現在所有的工作都很好,但是,如果if它正在迭代,另一行被添加到它使用的文件的底部,因爲它是迭代條件,它只是忽略它。批處理For循環不刷新它從

什麼我已經是這樣的:

@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在邏輯表達式中不起作用。

給任何人建議嗎?

回答

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。但是在append和delete之間有一個時間點,隊列編寫者可以在late.txt中附加一行。這條遲到的線路將被刪除而未經處理!

5)另一種可能性是寫入者可能試圖在隊列處理器正在刪除的過程中寫入late.txt。寫入將失敗,並且您的隊列將再次失去工作。

6)另一種可能性是隊列可能嘗試刪除late.txt,而隊列作者正在追加它。刪除將失敗,並且在下一次隊列處理器將late.txt附加到queue.txt時,最終將在隊列中出現重複項。

總之,併發問題可能會導致您的隊列中缺少工作以及隊列中的重複工作。只要你有多個進程同時修改一個文件,你必須建立某種鎖定機制來序列化這些事件。

您已經在使用SqlServer數據庫。最合理的做法是將您的隊列移出文件系統並移入數據庫。關係數據庫是從頭開始構建的,以處理併發性。

也就是說,只要你使用鎖定策略,在Windows批處理中使用文件作爲隊列並不困難。您必須確保您的隊列處理器和隊列編寫器遵循相同的鎖定策略。

下面是一個基於文件的解決方案。我假設你只有一個隊列處理器,可能還有多個隊列編寫器。通過額外的工作,您可以調整文件隊列解決方案以支持多個隊列處理器。但是多個隊列處理器可能更容易使用我在my first answer末尾描述的基於文件夾的隊列實現。

而不是讓隊列寫入者寫入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. 

如果你有一個獨特的進程ID,你可以把它寫內部塊中的queue.lock。然後,您可以從另一個窗口中鍵入queue.lock,以確定哪個進程當前擁有(或最近有)該鎖。如果某個進程掛起,那應該只是一個問題。

+0

哇,我可以說,出色的工作,你應得到這樣的獎勵,這是一個令人難以置信的工作量,我將通過這個工作,看看事情如何使用你的解決方案,但我的上帝,再次出色的工作!爲你明天+100! –

+0

@MylesGray - 酷。這很好,值得讚賞,而賞金是一個意想不到的甜蜜獎金 - 假設我的答案得到的票數最多:)代碼並不難,因爲我在20世紀80年代做過一些基於文件系統的隊列工作。但解釋確實需要時間。 *注意* - 自第一次發佈以來,我在queueWriter(條件轉移)的最後添加了一個快速錯誤修復程序。 – dbenham

+0

我可以儘早結束並無論如何都將獎勵給您 - 這就是我所期待的(需要24小時才能做到)。對於基於文件夾的解決方案,我認爲我只是交換文件夾中的inProcess.txt,queue.txt等? 你能解釋一下'2> nul(> queue.lock'的工作原理嗎? –

2

你是絕對正確的 - FOR/F循環等待IN()子句中的命令完成並在處理第1行之前緩存結果。如果您從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」文件夾中的不工作單元)以及哪些工作單元尚未處理(仍位於「隊列」文件夾中)。

+0

非常感謝你,但是我發現了一個更直接的方式(如果某件事在我的腳本中死去了),我會用你的想法與文件夾,但更好! –

0

好了,所以在soultion我的問題,我摸索出了添加一個名爲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 
+1

此解決方案存在一些重大問題。請參閱我的[第二個答案](http://stackoverflow.com/a/9048097/1012053)獲取更多信息 – dbenham

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文件底部的行將被立即讀取。

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

我知道它替換了整個文件,該部分所做的是檢查'late.txt'對於任何新行添加它們到'new.txt',並將當前'queue.txt'文件添加到'new.txt',然後用'new.txt.'的內容覆蓋'queue.txt'。 –

+0

@MylesGray:我剛纔注意到我的方法需要將_append_換行到'queue.txt'文件,而不是_replace_它。這個方法很有趣,因爲它完全避免了在讀取'queue.txt'文件的進程和任何其他進程之間的所有併發問題,並且它簡單快速。我添加了一個小例子,以便您可以看到我的方法可以執行的操作;該示例基於您的原始問題。 – Aacini