x2ts的事件总线

有的时候我们会需要在主线流程之外处理一些支线任务,比如用户注册的主线任务是检查和保存用户的各种资料,同时存在支线任务是给邀请此用户来注册的另一用户发送一条邀请成功通知。直观的写法是把发送通知的代码直接追加在注册程序的保存命令之后,这样的写法一个还不打紧,再多写几个就会破坏现有程序的可维护性,比如注册就赠送一个体验产品订单,还有注册成功要向统计系统发个消息通知……

基于事件模型来写这种支线程序可以让代码组织得更好。注册部分的代码只专注于注册该干的事情——检查数据、创建用户对象、设置用户资料、写入数据库,然后触发“用户注册”事件,后面的通知、馈赠、统计等支线行为,应该由监听用户注册事件的一众程序来处理,它们可以写在关联事件监听器的地方,也可以写在相应业务的模块里。

function notifyInvitor(event\user\Register $ev) {
    $user = $ev->user;
    if ($user->invitation) {
        //发送通知给邀请的用户
    }
}
X::bus()
    ->on('user.register', 'notifyInvitor')
    ->on('user.register', ['Product', 'presentTrial'])
    ->on('user.register', ['Statistic', 'userRegister'];

在用户注册的主业务代码中,只需要触发此事件即可:

//各种检查和赋值
$user->save();
X::bus()->dispatch(new event\user\Register([
    'dispatcher' => $this,
    'user'       => $user,
]));

x2ts框架的事件也已转向使用事件总线。所谓事件总线,意思是所有事件都在挂载在同一个对象上,并且所有事件都在同一个对象上触发,这样一来,开发事件处理函数的人无需了解事件触发的程序细节,只要根据文档选择适当的事件,即可开发回调函数,完成支线任务。所有的x2ts框架事件,都定义在x2ts开头的命名空间下;所有的应用层事件,都定义在event开头的命名空间下,代码保存在项目的protected/event目录中。

许多时候事件处理代码只是一些小函数,没有完整的class或者模块承载,为了更好的管理这些函数,项目加入了一个event\Setup类,位于protected/event/Setup.php里,在这个类里可以写静态方法作为事件监听器函数。还有一个静态方法setup()负责注册这些事件监听器。事件监听写的书写原则是为每一个任务编写一个静态方法,名称和要做的事情相符,比如notifyInvitor表示此方法会发送通知给邀请人,sendAskAnsweredNotify表示发送答疑回复通知等等。

x2ts日志的一些变化

这次重构x2ts框架的时候,日志模块也在大调之列。最主要的变化当属放弃了原来的直接写入日志文件的做法,改为使用monolog作为日志处理模块。monolog把记录日志这件事划分成了LoggerHandlerFormatter三个部分。Logger负责和应用程序对接,提供诸如noticewarning这样的简便接口,也提供addRecord这样的通用接口。Handler负责日志的处理和输出,可以输出到stderr,也可以把文件、网络、命名管道、syslog、消息队列、PHPConsole等等作为输出日志的目标。Formatter负责日志输出前的格式化,可以把日志文本和元信息(比如时间、进程号等)格式化成指定的样子。

以前日志函数在x2ts\Toolkit中提供,一个是log还有一个是trace,现在Logger是一个独立的x2ts组件类,它是核心组件之一,不论是否在配置中定义始终都可以使用(如果没有定义输出目标,默认会输出到stderr)。在日志消息等级方面,以前的DEBUG、NOTICE、WARNING、ERROR共4个级别被扩展成monolog所定义的8个级别:DEBUG、INFO、NOTICE、WARNING、ERROR、CRITICAL、ALERT、EMERGENCY。

调用日志的方式也改为使用logger组件:

X::logger()->info($someMessage);

为了方便日志在各种Handler中输出,原来日志中夹带的Unix Console颜色字符从日志中去掉了,现在的日志文件只有纯文本数据。如果想看带颜色的日志需要使用开发机上的colorlog脚本。

tail -f app.log | colorlog

现在日志的输出等级依然是debug模式输出DEBUG以上的日志,非debug模式输出NOTICE级别以上的日志。在新的级别体系中,INFO级别的日志可以用于区分框架输出的trace log

x2ts最近的几个大动作

最近在将x2ts由git submodule使用方式重构到composer上,顺便检讨之前设计不合理的地方并加以修订。预计将会有以下几大调整:

  1. 使用composer管理依赖
  2. 遵循PSR-1开发规范
  3. 使用PSR-4 autoload机制
  4. 日志模块基于monolog实现
  5. 配置不再保存于static变量中,迁移到Configuration类
  6. 加入事件支持和全局事件总线
  7. 路由器切分为路由和规则两部分
  8. Router和Action类中的事件改向全局事件总线触发
  9. 除MVC外的组件迁移到单独的库中,作为可选模块使用
  10. 加入Unit Test

PHP 7.0.16 在阿里云容器服务中Could not gather sufficient random data问题的解决办法

最近一个在公司内部环境测试正常的PHP7.0.16容器发布到阿里云容器服务后,调用random_bytes函数回报Could not gather sufficient random data错误。上网搜了一圏看到是这个Patch导致的。如果PHP在新版系统内核中编译,PHP会使用新版内核中提供的getrandom调用来取得随机数,而不会读取/dev/urandom,如果把它移到旧版内核的系统上运行,random_intrandom_bytes函数就会报Could not gather sufficient random data。

阿里云的容器服务默认用的是Ubuntu 14.04 3.13.60-generic版本的内核,我的PHP容器是在Ubuntu 16.04 4.9.12-moby内核中编译构建的,所以会触发这个问题。解决方法是升级阿里云的容器服务节点,把内核升级到4.x之后此问题就没有发生了。希望未来的PHP版本会提供getrandom失败后的fallback方案。

清理mac上Docker占用的磁盘空间

Docker依赖Linux系统的cgroup实现,在mac系统中运行的时候,Docker会启动一个虚拟机中的Linux内核,并在硬盘上放一个qcow2格式的磁盘镜像文件。这个文件会随着Docker的使用不断膨胀,即使删除不用的Docker Image和Container也不会缩小。我在测试完一个自动化工具的Dockerfile改写之后,Docker.qcow2文件就达到42GB了。

Google搜了一圏发现一个叫Théo Chamley的法国人写了一个脚本释放Docker.qcow2文件占用的空间。基本原理是用docker save命令保存要保留的image,然后关闭Docker,删除Docker.qcow2,再启动Docker,它会自动重建,最后用docker load命令恢复保留的image。

下面是这哥儿们写的脚本。

#!/bin/bash

# Copyright 2017 Théo Chamley
# Permission is hereby granted, free of charge, to any person obtaining a copy of 
# this software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

IMAGES=$@

echo "This will remove all your current containers and images except for:"
echo ${IMAGES}
read -p "Are you sure? [yes/NO] " -n 1 -r
echo    # (optional) move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
    exit 1
fi


TMP_DIR=$(mktemp -d)

pushd $TMP_DIR >/dev/null

open -a Docker
echo "=> Saving the specified images"
for image in ${IMAGES}; do
    echo "==> Saving ${image}"
    tar=$(echo -n ${image} | base64)
    docker save -o ${tar}.tar ${image}
    echo "==> Done."
done

echo "=> Cleaning up"
echo -n "==> Quiting Docker"
osascript -e 'quit app "Docker"'
while docker info >/dev/null 2>&1; do
    echo -n "."
    sleep 1
done;
echo ""

echo "==> Removing Docker.qcow2 file"
rm ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2

echo "==> Launching Docker"
open -a Docker
echo -n "==> Waiting for Docker to start"
until docker info >/dev/null 2>&1; do
    echo -n "."
    sleep 1
done;
echo ""

echo "=> Done."

echo "=> Loading saved images"
for image in ${IMAGES}; do
    echo "==> Loading ${image}"
    tar=$(echo -n ${image} | base64)
    docker load -q -i ${tar}.tar || exit 1
    echo "==> Done."
done

popd >/dev/null
rm -r ${TMP_DIR}

Théo Chamley的博客原文