Python任务调度程序Luigi可以检测间接依赖吗?

时间:2017-02-26 07:04:21

标签: python dependencies scheduled-tasks luigi

简短版本:

Python中是否有可以执行gmake的任务调度程序?特别是,我需要一个以递归方式解析依赖关系的任务调度程序。我查看了Luigi,但似乎只解决了直接的依赖关系。

长版:

我正在尝试构建一个以预定义顺序处理大量数据文件的工作流,后面的任务可能直接依赖于某些早期任务的输出,但反过来,这些输出的正确性依赖甚至更早的任务。

例如,让我们考虑如下的依赖关系图:

A&lt; -B < - C

当我从任务C请求结果时,Luigi将自动安排B,然后由于B依赖于A,它将安排A.所以最终的运行顺序将是[A,B,C]。每个任务都将创建一个官方输出文件,作为成功执行的标志。这对于第一次运行来说很好。

现在,假设我在任务A的输入数据中犯了一个错误。显然,我需要再次重新运行整个链。但是,简单地从A中删除输出文件就不会起作用。因为Luigi看到了B和C的输出,并得出结论,任务C的要求已经完成,并且不需要运行。我必须从 ALL 中删除依赖于A的任务的输出文件,以便它们再次运行。在简单的情况下,我必须删除A,B和C中的所有输出文件,以便Luigi检测到对A的更改。

这是一个非常不方便的功能。如果我有几十个或几百个相互之间具有相当复杂依赖关系的任务,那么当需要重新运行其中一个任务时,很难确定哪些任务受到影响。对于任务调度程序并具有解析依赖关系的能力,我希望Luigi能够采用类似于GNU-Make的方式,其中依赖关系以递归方式检查,并且当最深的源文件之一被更改时,将重建最终目标。

我想知道是否有人可以就此问题提供一些建议。我错过了Luigi的一些关键功能吗?是否还有其他任务调度程序可以充当gmake?我对基于Python的软件包特别感兴趣,并且更喜欢那些支持Windows。

非常感谢!

2 个答案:

答案 0 :(得分:2)

似乎可以通过覆盖完成任务的方法来实现。您必须在依赖图中一直向下应用它。

def complete(self):
    outputs = self.flatten(self.output())
    if not all(map(lambda output: output.exists(), outputs)):
        return False
    for task in self.flatten(self.requires()):
        if not task.complete():
            for output in outputs:
                if output.exists():
                    output.remove()
            return False
    return True

答案 1 :(得分:1)

实际上这很不方便,d6tflow检查所有上游依赖项的完整性,而不仅仅是TaskC的输出是否存在。如果重置TaskA,TaskC也将不完整并自动重新运行。

# reset TaskA => makes TaskC incomplete
TaskA().invalidate() 
d6tflow.preview(TaskC()) # all tasks pending

有关更多详细信息,请参见下面的完整示例和d6tflow docs

import d6tflow
import pandas as pd

class TaskA(d6tflow.tasks.TaskCachePandas):  # save dataframe in memory

    def run(self):        
        self.save(pd.DataFrame({'a':range(10)})) # quickly save dataframe

class TaskB(d6tflow.tasks.TaskCachePandas):

    def requires(self):
        return TaskA() # define dependency

    def run(self):
        df = self.input().load() # quickly load required data
        df = df*2
        self.save(df)

class TaskC(d6tflow.tasks.TaskCachePandas):

    def requires(self):
        return TaskB()

    def run(self):
        df = self.input().load() 
        df = df*2
        self.save(df)

# Check task dependencies and their execution status
d6tflow.preview(TaskC())
'''
└─--[TaskC-{} (PENDING)]
   └─--[TaskB-{} (PENDING)]
      └─--[TaskA-{} (PENDING)]
'''

# Execute the model training task including dependencies
d6tflow.run(TaskC())

'''
===== Luigi Execution Summary =====

Scheduled 3 tasks of which:
* 3 ran successfully:
    - 1 TaskA()
    - 1 TaskB()
    - 1 TaskC()
'''

# all tasks complete
d6tflow.preview(TaskC())

'''
└─--[TaskC-{} (COMPLETE)]
   └─--[TaskB-{} (COMPLETE)]
      └─--[TaskA-{} (COMPLETE)]
'''

# reset TaskA => makes TaskC incomplete
TaskA().invalidate() 
d6tflow.preview(TaskC())
'''
└─--[TaskC-{} (PENDING)]
   └─--[TaskB-{} (PENDING)]
      └─--[TaskA-{} (PENDING)]
'''