第一讲写在了微博上,篇幅不长,讲了一个api或function call返回error时要保证检查顺序的问题。node
这件事情看起来不重要,也很难打动你;因此须要讲一个positive case来改变你对spec和测试的见解。编程
这也是我这两天要写的代码,一个在服务器端copy/move一组文件或文件夹的功能,我是作nas的,在2018年书写在1998年各大操做系统就完成的功能,不一样之处在于,如今我们restful和microservice了。api
事实上咱们在过去的两年写了海量的相似的业务,各类异步并发过程组合,各类流的mux/demux,各类lazy和各类在并发状况下的错误处理,随着对并发状态机组合的了解愈来愈深刻,常见的应用场景都没什么问题;直到遇到这个copy功能的实现。服务器
这个功能不一样在哪里呢?象文件批量上传这样的功能,系统对客户端而言是blackbox,你内部有多少并发或串行组合状态机,外部是看不见的,最可能是遇到错误时内部要优雅的停下来。restful
可是递归式的复制文件和文件夹不同,客户端那里蹲着一个充满好奇心的灵长目动物,它想看见你的内部过程,或者至少是一部分;同时这个操做还不可避免的会遇到各类冲突和错误,同名的文件和文件夹,由于系统其余用户的并发操做致使的一些失败,甚至是这个用户忽然被管理员删除了,诸如此类。并发
理论上它等价于一个状态机把状态暴露出来,可是这里有个问题,外部的观察实际上是异步的,内部的不少状态存在自发迁移,黑盒测试的代码在polling和拿到api结果时再想去assert系统数据(文件系统上的数据,不是服务器内部的状
态),可能已经发生了变化。异步
可是业务上说,若是所以把服务端的行为设计成串行和单步的,虽然得到了良好的可测试性,缺没有了实用价值,效率牺牲太多。函数
因此这是一个很是好的如何specify系统行为的问题;之因此用specify这个词,是由于咱们要指定的仍然是黑盒意义上客户端和服务端的合约(contract),而不是服务端内部的状态机实现,若是是后者,咱们就是在说design了。高并发
咱们的目的是在系统的实用性和黑盒可测试性上找到一个平衡。没有一个好的合约,就像一个定理缺少证实,你只好随机的找一大堆test data和test case来试行为和结果,俗称quality assurance。性能
测试一个函数的基本逻辑是y = f(x),咱们先找到一个数学函数f,给定x就能计算出y,而后咱们去测代码实现的f',检查对于同一个x,它是否和咱们定义的数学函数给出的结果一致。
在这里咱们剥离了测试数据x和系统行为定义f,f即spec。
对于api测试,咱们把参数拓展为[a,b,c,d]理论:
a: 系统预置状态
b: 调用api的参数
c: api返回结果
d: 系统结束状态
参数多了点,但本质是同样的,系统的spec是存在f: (a,b) => (c,d),这是肯定的。
咱们来脑部一下加入程序跑一下应该获得什么结果。
系统的初始状态是存在一个源文件夹src,一个目标文件夹dst,以及用户须要从src中copy到dst中的一些entries,多是子文件夹,多是文件,这个列表不该该为空,不然没有业务意义。
对于测试程序而言,[src, dst, entries]是参数,即上面所述的b;a呢?a能够理解为在开始时src和dst的整个hierarchy。
好了咱们有了a和b,咱们期待什么样的c和d?
假如被测程序没有任何限制,即遇到多少个命名冲突整个任务应该停下来的状况,再假如没有遇到文件系统访问错误,这个操做到最后是完成了尽量多的能够成功复制的文件和文件夹,可是也找到了全部遇到冲突没法继续的状况,等待用户决策。
在这个假设下咱们是可以获得c和d的定义的,c能够定义为所有冲突状况的列表,d是最终任务停下来时的结果。
这是一个spec吗?咱们说是的,由于abcd有无歧义的定义,并且是能够实现这个函数的,在src和dst两个tree上visit一遍就能根据a/b计算出c/d。
可是这个spec有两个问题:
若是你在任务中间去polling服务器,即便服务器只暴露出现命名冲突的文件夹和文件给客户端,这个列表中的内容是稳定的,客户端不操做其中的条目不会自行消失;可是条目的出现顺序能够是随机的;这种随机性在实现角度看没有任何问题,彻底不至于由于乱序复制一个文件夹中的内容影响执行结果的正确性。
可是在合约角度看,它难以assert,一方面由于随机性,另外一方面由于服务器不会停下来等待客户端去assert。
去除随机性很简单,例如咱们能够约定服务器端的执行必须按照depth first/previsit的顺序进行,这对服务器知足功能正确性而言是没必要要的spec,可是对于test它会让测试代码方便不少,只要没有显著的性能影响,咱们不介意把这样一条rule写到spec里去。
但另外一方面会比较麻烦,如何让服务器停下来容许客户端assert?
熟悉生产者消费者模式的人一眼就能看出这里须要一个调度器;若是不熟悉这个模式能够这样理解,在一个子任务完成后,代码中会有继续执行下一个任务的逻辑,只要在这里插入过程,便可让continuation中断和恢复。
若是没有额外的限制,这个continuation的实现方法不少:
都OK,可是谁该win design呢?
使用全局调度器的1和3都是能够容易知足stop任务执行的要求的。若是没有一些错误处理要求1是OK的,若是遇到父文件夹发生文件夹丢失之类的上下文相关错误,使用tree结构处理更方便一些。
具体怎么实现不是重点,这里想强调的是:如何break复杂过程,让更细粒度的testing/verification可行。
好了,有了这两个新规则咱们再看一下问题:
首先,它应该提供一个stepping模式;在任务初始建立时处于stopped状态;
对于建立任务的需求,咱们的a和以前同样,b是建立任务的参数,c是返回的任务结果,这个时候什么实质性的工做也没有作,只是server端有个任务描述,处于stopped状态;而d和a同样。
而后咱们有了第二个客户端动做,姑且称为step;step的意思是server端能够根据调度规则选择几个任务开始,一直执行到这些任务完成,可是调度器不工做,因此不会有新的任务产生;step在完成时马上返回内部任务状态,它能够列入spec,也能够不列入,取决于你想在多大程度上assert系统行为,但本质上这是灰盒的。
第三个客户端动做是watch;服务器在收到客户端的watch调用时,若是已经完成了一个step的全部操做,进入stopped状态,则马上返回状态描述;若是没有,它应该等到step完成时再返回,这样对客户端来讲比较容易使用。
step-watch构成了一个cycle,若是不想观察step以后哪些任务在执行的状态,step-watch能够合并成一个api调用。无论怎样,咱们获得了一个细粒度的assert能力,在每次step-watch结束时,能够assert c和d了。由于server停下来了,d能够assert。
那么在这个设计下,服务器端的每次并发多少,是一个参数,或者说policy,若是不区分文件和文件夹并发数设置为1,它就退化成了顺序执行;实际使用中会用到的并发限制,对node.js而言,建立文件夹能够是个很大的值甚至不作限制,若是你不介意在任务完全失败时预先建立了海量的空文件夹的话;复制文件高并发没有意义,瓶颈在磁盘io那里,通常2个并发就够了。
因此如今你能脑补出来的执行过程是:每次step,调度器要按照previsit原则填充指定的任务,当他们结束时,不管是遇到冲突仍是成功完成任务,结果都是容易预先计算的。
可是还有一个问题:这样一步一步执行的结果,真的和实际使用时,每一个子任务结束时都kick调度器的结果同样吗?能保证这一点吗?
这须要把调度器的要求再提升一点:即便系统不处于stopped的状态,仍然能够调度,离并发限制差多少就该填充多少任务进去。在这种状况下,你能够连续step屡次最终只watch一次。每次step的结果能够assert c,最终的watch结果能够assert c和d。
这是final的保证吗?也不是,除非你真的能坐下来给一个数学上的proof,不然都不算能证实其正确性。咱们仍然或多或少的须要不那么可靠的直觉。
可是把spec强化到这个程度,对我来讲,它的每次执行能够算是很容易预测(计算)出来的,这意味着step by step的spec函数能够书写、易读、且状态打印结果是比较易懂的。在工程上,这已经很好了。
说一个小问题。
好比在SRS文档上写了一个限制,在整个任务遇到5个冲突时应该停下来。
这个限制多是灵长目客户出了钱要必定实现的;实现也不难,调度器调度的时候要考虑还剩多少个能够冲突的并发可用,而不是预设的并发数限制,换句话说,若是已经有4个冲突了,这个程序就只能堕落成one by one的顺序执行了,由于若是并发了两个,两个都冲突,这条愚蠢的限制就无法meet了。
可是若是这个限制是假装成灵长目客户的另外一个灵长目同事提出的(俗称产品经理),你最好跟他商量商量,手里要拎着一个棒子,上面刻着persuader。由于这种需求就是在没有推敲细节时拍脑壳想出来的,它没考虑对性能的影响。严格遵照这个5没有什么意义,除非团队用易经指导编程。
说完了。小结一下:
Matias Duarte说:
Design is all about finding solutions within constraints. If there were no constraints, it is not design - it's art.
对于代码来怎么理解这句话呢?就是不要把代码怎么写都是能够的挂在嘴边,找到那些你还没发现的constraint,才是区分好的design和坏的design的关键;在这篇文章里,将的就是从spec/testing的角度去经过加入更多的constraint,让design变得更容易test/verification。
Tony Hoare说:
There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.
Hoare说的so simple that there are obviously no deficiencies
怎么理解呢?如何作到呢?就是你的spec合约如此的简单,即便对于复杂行为实现,你仍然能够break it down,用概括法获得足够简单的验证方法 - 尤为是在遇到复杂问题,在实现层面难以简化的时候。