Long polling

这两天在公司的项目里用了 Long polling,了解了它的实现原理,其实不像它的名字那么玄乎,只是 Ajax 和 HTTP 的类似小妙招的办法。

先解释一下 Long polling 是什么:

首先得说到传统的 Polling,Polling 是 Ajax 隔一段时间去抓取服务器上的数据,检查数据是否更新,但这样有很大问题,首先是每次请求会实用一个  HTTP request,对应的服务器就得建一个新的线程来处理这个 HTTP request,消耗网络流量、服务器资源不说,绝大多数情况下,数据短时间内是不会更新的,也就是说绝大多数的 Ajax 请求都只能无功而返。

而 Long polling 就是用来解决这个问题的。


它的核心是:
1. 做一个超时非常长的 Ajax 请求,并且在错误捕获代码里不断执行自己。2. CGI 部分接收到请求后在限定的时间内(Ajax 超时时间内)每隔一段时间(例如一秒)对数据库进行查询,可以使用 sleep 类似的方法。
3. 如果有新的数据则返回,如果即将到 Ajax 超时的时间则返回一个错误值,比如 404,这样那个非常长的 Ajax 请求会再发一个过来,继续查询。

这个办法的最大价值是有效减少了 HTTP 请求数,对服务器而言就不用开启新的线程去处理它,旧的线程如果不是因为超时,则只会在数据已经更新的情况下返回数据。可以节约大量资源,而且实时性更高。

目前,人人网的消息提示、Web 版阿里旺旺、新浪微博的新微博提示应该都是使用这种方法做的。

但这个办法对开发有一定困扰,Django 内置的 Web service 是单线程的,一个非常长的 Ajax 请求会占用那唯一一个线程,导致别的请求无法响应。

所以最好做好两套配置,一套用于产品环境的多线程环境,一个用于开发的单线程环境。

我依然对那位在现有架构下想出这种办法的人表示钦佩。

这儿有范例代码,简单又实用:http://stackoverflow.com/questions/333664/simple-long-polling-example-code

Posted by K*K Fri, 08 Jul 2011 17:49:20 +0800


几个手机的性能测试。

趁着职务之便,能接触到不少手机,所以本着见啥测啥的精神,就有了此文。

目前测试软件只有 Quadrant Advanced,如果谁知道有其它测试软件,尤其针对应用程序 Load 时间的,欢迎推荐。

最后声明一下:本测试纯属个人兴趣,如果对某商品起到了促销作用纯属巧合,影响了某商品的销量也概不负责啊。

测试项目\测试机型 Sharp 8128u Nexus One HTC Incredible Samsung i9000
设备配置 CPU 高通 7x27(ARMv6) 高通 QSD8250(ARMv7) 高通 QSD8650(ARMv7) 三星 GT-I9000
主频 122.88MHZ-600MHz 245MHz-1113.6MHz 128Mhz-1228.8MHz 100MHz-1000Mhz
BogoMIPS 429.72 662.40 662.40 99.73
机身内存 407316KB 412052KB 422908KB 333416KB
屏幕分辨率 800x480 800x480 800x480 800x480
GPU Adreno ?? Adreno 200 Adreno 200 PowerVR SGX 540
OS 点心OS(2.1-update1) MIUI 1.3.18(2.2.1) HTC Sense 2.0 TouchWiz 2.1
Kernel 2.6.29-perf 2.6.35.9 2.6.32.28-ck2-CFS 2.6.29
Quadrant
Advanced
1.1.6
(2010-12-30)
总分 272 1279 1372 850
CPU (因AAC解码崩溃无法继续 ) 4050 4768 716
Memory 617 1042 829 1765
I/O 382 654 575 640
2D 100 263 276 302
3D 261 391 392 827

 

Posted by K*K Wed, 23 Mar 2011 17:44:56 +0800


旧年总结和新年计划。

今天大年初一,祝大家新年走大运,工资节节高。

白天串了一天亲戚,晚上来想想一下去年都做了些什么比较有意义的事情,新年又应该做些什么了。

2010 旧年总结:

职业上

  • Nitrate 进入稳定阶段,成功将推入公司内部,将旧的 Testopia 关掉,成为测试部唯一测试用例管理工具。
  • 成功将 Nitrate 推入 Fedorahosted,正式开放源代码,并计划在 Fedora 15 的时候推广到 Fedora 社区内部测试,并计划在 Fedora 16 的时候正式使用,替代掉现有 Wiki 系统。
  • 成功拿下 RHCE 和 Certified Scrum Master 认证,前者早就拿过只是刷新一下,后者让我对软件工程管理有了新的认识。
  • 因感到个人技术瓶颈,离开了 Red Hat,寻找新的发展。

生活上

  • 买了一套房子,正式成为房奴。
  • 经过了近三年的发展,和女朋友感情进入平稳阶段,吵闹的事情减少了很多。
  • 爷爷经历了84危机,好在平稳度过,目前已经无恙。

2011 新年计划:

职业上

  • 寻找新的发展,并寻找转型机会,希望能将 Red Hat 先进的工作流和管理经验带入其它公司。
  • 继续参加培训,提高自身水平。软件开发、管理课程、礼仪、沟通与交流、心理学/哲学方面都是不错的主题。
  • 探索一些新的领域,例如互联网,移动开发,高负载下的性能优化等等。
  • 启动新的项目,作为个人历练用,因为是个人项目,所以有更多“研发”的工作,用一些新的经验,新的思路,新的想法。已经有了目标,一是重写 Nitrate,二是编写一个新的项目管理工具,将 Scrum 的理念带入其中。
  • 参与 Django 项目开发,适时提交补丁。

生活上

  • 如果不出问题,应该会在今年结婚。
  • 去考一个车本,驾驶是现在必备的技能。
  • 学学喝酒。
  • 阅读更多、更广泛的书籍,不再局限于软件开发方面。
  • 去旅游。
  • 学习一下乐器,家里的 MIDI 键盘和吉他都买了好久了,一直没动力去认真学,打算用学习编写软件的方法,跳过理论,直接实践了。
  • 照顾好家人,常回家看看,尤其家里老人,可能时日无多。

希望到明年这个时候,回过头来,计划都能够完成。

Posted by K*K Fri, 04 Feb 2011 09:32:36 +0800


春运又到了,献上更新版抓黄牛脚本。

好不容易搞定了火车票(当然不是通过酷讯或者黄牛),把去年写过的抓黄牛脚本重写了一下,提供给各位还在等待购买火车票的 Programmer 使用。说是抓黄牛,自然还包括普通转票者。原理还是通过轮询酷讯网站上的内容,但是增加了几个新特性:

  • 用 re 提供的正则表达式替换掉了 SGMLParser 提高效率
  • 可以轮询多个地址了,比如我到吉安和井冈山都可以,所以我要遍历两个地址
  • 可以将转向链接直接打印在屏幕上了
  • 提供了 Python 3 的 Package 级支持,但是因为 re 模块变更,正则表达式在 Python 3 里无法运行,暂时没心思更新了。

尽管酷讯推出了秒杀器,不过还是觉得不妥,一是没任何输出,谁知道它是否真的能秒到,二是不跨平台,在 Mac 和 Linux 上暂时无法使用。

Patches are welcome. :-)

#!/usr/bin/python
# encoding: utf-8
#
# Catch the yellow cattles script
#
# Author: Xuqing Kuang <xuqingkuang@gmail.com>
# New features:
# * Use regexp to instead of SGMLParser for performance
# * Polling multiple URL at one time.
# * Print out the redirect URL.
# * Basic packages compatible with Python 3
# TODO:
# * Use one regexp to split the href and text of link
# * Update re package usage to compatible with Python 3

import time
import os
import re

try:
    import urllib2 as urllib
except ImportError: # Python 3 compatible
    import urllib.request, urllib.error

urls = (
    "http://piao.kuxun.cn/beijing-jinggangshan/",
    "http://piao.kuxun.cn/beijing-jian/",

)

keyword = '3张'
sequence = 60

class TrainTicket(object):
    """
    Catch the yellow cattle
    """
    def __init__(self, urls, keyword, sequence = 60):
        self.urls = urls
        self.keyword = keyword
        self.sequence = sequence
        self.cache=[]
        self.html = ''
        self.links = []
        if hasattr(urllib, 'build_opener'):
            self.opener = urllib.build_opener()
        else: # Python 3 compatible
            self.opener = urllib.request.build_opener()
        self.result = []
        self.re_links = re.compile('<a.*?href=.*?<\/a>', re.I)
        # self.re_element = re.compile('', re.I) # Hardcode at following
        self.requests = []
        for url in urls:
            if hasattr(urllib, 'Request'):
                request = urllib.Request(url)
            else: # Python 3 compatible
                request = urllib.request.Request(url)

            request.add_header('User-Agent', 'Mozilla/5.0')
            self.requests.append(request)

    def get_page(self, request):
        """
        Open the page.
        """
        try:
            self.html = self.opener.open(request).read()
        except urllib.HTTPError:
            return False
        return self.html

    def get_links(self, html = ''):
        """
        Process the page, get all of links
        """
        if not html:
            html = self.html
        self.links = self.re_links.findall(html)
        return self.links

    def get_element(self, link = ''):
        """
        Process the link generated by self.get_links().
        Return list of the href and text
        """
        # FIXME: have no idea how to split the href and text with one regex
        # So use two regex for temporary solution
        href = re.findall('(?<=href=").*?(?=")', link) # Get the href attribute
        if not href:                                   # Process the no href attr
            href = ['']
        text = re.split('(<.*?>)', link)[2]            # Get the text of link a.
        href.append(text)                              # Append to the list.
        return href

    def get_ticket(self, request = None):
        """
        Generate the data structure of tickets for each URL.
        """
        if not request:
            request = self.requests[0]

        self.get_page(request)
        self.get_links()

        i = 0
        while i < len(self.links):
            link = self.get_element(self.links[i])
            if not link:
                continue
            url = link[0]
            name = link[1]
            if name and name.find(keyword) >= 0 and url not in self.cache:
                self.result.append((
                    i, name, url,
                ))
                self.cache.append(url)
            i += 1
        return self.result

    def print_tickets(self):
        """
        Process all of URLS and print out the tickets information.
        """
        while 1:
            self.result = []
            try:
                print('Begin retrive')
                for request in self.requests:
                    print('Begin scan %s' % request.get_full_url())
                    self.get_ticket(request)
                    print('Found %s urls.' % len(self.links))
                    for r in self.result:
                        print('Index: %s\nName: %s\nURL: %s\n' % (
                            r[0], r[1], r[2]
                        ))
                print('Scan finished, begin sleep %s seconds' % self.sequence)
                time.sleep(self.sequence)
            except KeyboardInterrupt:
                exit()
            except:
                raise

if __name__ == '__main__':
    tt = TrainTicket(urls, keyword, sequence)
    tt.print_tickets()

 

Posted by K*K Tue, 18 Jan 2011 22:54:18 +0800


高内聚、低耦合、SOA 和测试驱动。

软件工程:

随着软件工程的不断膨胀,功能的扩展变得愈发困难,因为增加一点点小功能而导致的 Regression 可能会越来越多,同时,因为项目越来越大,参与的人越来越多,代码的结构化、模块化便成了高质量产品非常重要的特性之一。

高内聚和低耦合,其实是好的模块化编程必须具备的两项特点,高内聚,我的理解模块独立完成特定,不重复实现其他模块已经实现过的逻辑,而低耦合,即模块与模块之间的直接连接要尽量低下,耦合性高,会导致牵一发而动全身,将导致未来的代码维护和功能扩展愈发艰难。

最简单的模块化即现在 Web 开发的 MVC 架构,数据库的建模部分由 Model 完成,页面展示由  Views 完成,而之间的协调和逻辑关系由 Controller 完成,以一个 Product 项目的创建为例,它会在创建时同时创建好另外两个字段的默认值 - Version 和 Component,当 Views 接收到用户的输入后,然后将它传递给 Controller,Controller 将会判断输入的合法性并同时创建好另外两个字段的类,再通过调用 Models 提供的方法将这三个类在数据库中持久化下来。

这需要在开发前定一套统一的 API,这样各个模块才能无障碍地随意调用。

那么这样的好处在哪里?其实是非常明显的,页面显示部分不用关注数据库会提供什么养的类型,而数据库也不用关注用户会提供什么格式的数据,都会因为中间 Controller 的调度和转化被统一起来,进而达到程序的模块化,以及开发人员的专职化。

以我自身经验为例,如果要和前端配合同时完成一套功能,我们会约定好我给他什么样的 Ajax method,参数是什么,返回的是什么样的格式,这样我们两个能够同时编写不同的代码,但到功能完成之时,将两个代码放在一起,直接就能运行起来。

SOA:

那么程序与程序之间的整合,使之同时完成一项功能如何才能做到?

传统的方法有提供 RPC 接口,轮询数据库或者文件系统,但是问题在哪里?

  • XML-RPC 同样会出现错误,而且缺乏 Error handler
  • 数据库和文件系统的轮询有很高的风险,首先轮询文件系统就以为着这个目录里的文件要小心放置了,因为不小心就会被程序轮询到,而因为轮询造成的数据库损坏的可能性更加难以纠正。
  • 很低的内聚性,很高的耦合性,为了同步两个程序间的数据,可能得克隆一份对方程序里的数据结构和逻辑方法,一旦任何一方的数据结构或逻辑改变,将导致同步失败。
  • 不实时,因为轮询总是有时间间隔的,轮询频繁会导致系统负载加重,轮询频率低了又容易等待时间过长。

那么程序之间的整合如何才能更好地做到,这就需要将 SOA 的思想引入进来了,SOA - Service Orient Architechture,面向服务的架构,程序与程序之间不再是程序,而是一个个的服务,这个服务可以通过 RPC 来实现对自身方法的调用,但是还有很重要的一点,是要将自身正在做的事情告诉别的程序。

下图是 Nitrate 项目目前的结构,其实我觉得它是模块化开发的一个典范了。

Nitrate 是标准的 MVC 架构,我来解释一下它的工作流,除了基本的 Web UI 界面和 XML-RPC 界面外,我们还提供了 Global signal processor(后简称 GSP)用于监听所有 Models 的信号,目前信号有三种 - Initial,对应于数据库的 SELECT 语句,Create 对应于数据库的 Insert 语句,Update 对应于数据库的 Update 语句,所有被注册的 Models 一旦发生以上动作将会出发对应的信号,然后 GSP 将信号推入 Plugin support(后简称PS),由它以线程方式将 Signal,Models 和触发消息的 Model Instance 交由 Plugin 处理。

这里我将 Plugin 分为了两类:

一类是 Special plugin,用于整合一些特殊的程序,这些程序不带有消息队列支持,它是以传统工具整合方式工作的,接受到信号后会根据已经定义好的参数判断信号是否符合条件,如符合则通过该 Special 程序提供的 RPC 接口将 TCMS 中的改动同步进该程序中,事实上该方法危险性较高,而且需要将对方的数据库表格和参数定义在插件里。

第二类是 Messaging queue plugin,这是我最偏好的方法,目前支持的 Messaging queue(以后简称 MQ)是 QPID,它将 Models 的 Signal 转译成 MQ 的数据格式,然后直接推入 MQ,然后别的需要 Nitrate 数据的程序可以提供第三方的一个 Sync middleware,作为两个程序间的桥梁对数据进行选择性的同步,这样的好处是逻辑不用重复实现,双方共享的数据之需要存在 Sync middleware 程序中即可,不影响双方逻辑和数据库结构。而 Nitrate 本身也提供了 TCMS Messenging listener 用于监听 MQ 内的 message,例如如果 Bugzilla 新创建了一个 Product,Nitrate 将很快便能监听到并在数据库中创建一份相同的 Product 供用户使用,全程自动化,智能化,无人干预。

“能听能说”的程序才能较好地整合到其它程序中,“听” 提供了 XML-RPC 接口,别的程序可以通过它更改 Nitrate 中的数据,“说”提供了 MQ 的支持,别的程序可以监听 Nitrate 中数据的变化,并以此为依据对自身数据进行添加、更改,而且不用存储对方数据表,不用了解对方的业务逻辑,只需要写好中间的 Sync middleware 即可,好处多多,其乐无穷。

测试驱动

在之前的 Scrum 培训总结中,我说到了 CI(Continuous Integration)是 Scrum 所要求必备的,那么为什么单元测试如此重要?

下面的图可以充分说明这点:

尽管手工测试可以降低编码时间,但这也仅仅是在项目非常非常小,而且没有后期维护成本的情况下。从上图可以看出,在第一个迭代的时候,开发如果和测试的时间是一样长,这或许可以生产出一个高质量的产品,但是到第二个迭代的时候,开发人员仅仅向软件中加入了很少的 Feature,但是测试却不能减少他们的工作量,依然要进行一遍完整的测试,而且还要检查是否有因为新加入的 Feature 导致的 Regression,到第三个迭代的时候开发的时间更加少,到第五个迭代后开发已经完全没用了,尽管测试占用的时间比例很大,但是依然无法保证测试完成,产品推到生产环境下会发现问题多多,开发人员就成了救火队员,下个迭代可能要用整个迭代的时间,去修上个迭代造成的 Bug,软件将停滞不前。

那么针对不同类型的语言和软件类型,有什么 Unit Testing 的方式呢?

这要从语言的类型和特点说起了。

* HTML,CSS - 前段,结构远远大于逻辑的语言。因为浏览器的兼容性问题,它是最难进行自动化测试的语言,但是依然是有办法的,可以使用类似 Dogtail 类的自动UI点击程序,判断 Console 中的错误输出,或者使用 W3C Validator 一类的工具,对它进行错误判断。

* Python/PHP/Java - 后端,结构和逻辑并重的语言。其实它是最好编写 Unit testing 的语言,因为它只负责一件事情:提供 API 供调用,我们只需要模拟输入,判断程序是否会出现 Error 即可,Django 提供了很好的 Unit testing 框架,可以模拟客户端的提交数据,以检测 Views 和 Controller 能否正常工作,Models 能否对数据库正常读写,或者 ForeignKey 的正确性也可以通过 UT 进行验证,Java 也提供了 JUnit 等工具。

* Javascript/其它 UI 语言 - 前端,结构和逻辑并重的语言。这是最有意思的地方,很多人说 UI 类的语言的 UT 非常难以编写,因为无法验证最终的正确性,其实仔细观察就可以看出,这类语言,其实只是调用了后端提供的 API,也就是说,对于此类语言,一检查提供给后端的 API 的参数是否正确,二检查能否将后端的返回正常生成数据结构并反应在页面上即可。前者依然可以通过检查后端的出错情况,而后者可以通过更改编码方式,将逻辑层和显示层拆分解决,例如后端返回的数据需要生成一个按钮,只需要将生成按钮的代码拆分,并通过单元测试检查按钮是否正常生成并符合条件即可,因为只要能正常生成,基本上现实在页面上不会出太大问题,这是基本功能了。

另外说一句有意思的事情,前两天听说了另一种 UT 的方式,名称叫 Monkey,写一个程序在屏幕上随机乱点,只要程序能稳定跑上几个小时就算通过,这来源于让猴子敲键盘,总有一天能敲出莎士比亚全集的传说。这也算是很有创意了,只是我觉得这不能叫 UT,可能只能叫 RT(Random Testing)了,缺点很明显,一是无法针对某个功能,二难以重现(可重现的 Bug 对开发人员很重要),三难以统计。

Posted by K*K Mon, 27 Dec 2010 01:36:18 +0800


字符串反转和判断素数的 Python 语言算法范例

第一个是反转字符串,前两种是我出来丢人的,请直接看第三种。

方法1:

取出最大长度并 -1,以此为索引,依次递减,并将结果加入数组。

def reverse(string):
    if not string:
        return string
    str_list = []
    i = len(string) - 1
    while(i >= 0):
        str_list.append(string[i])
        i -= 1
    return ''.join(str_list)

方法2:

类似 C 中的位移操作,使用一个中间变量 t 记录临时值,然后将前后为置换,时间主要花在将字符串转换成数组上,实际遍历只需要上面方法的一半,但比上面方法要多消耗一个临时变量 t 的内存。当然,如果用 C 就可以直接指向内存,这样最节省内存,但是 Python 可能没更好办法。

但因为对 str 操作会造成 TypeError,所以还需要组成一个临时数组并在最后合并。

def reverse2(string):
    if not string:
        return string
    
    idx = 0
    length = len(string) - 1
    h_length = length / 2 # Half of length
    # 'str' object does not support item assignment for Python
    # Use C to solve it will be better.
    # So we convert it to be a list from the string.
    str_list = [x for x in string]
    
    while(idx <= h_length):
        t_idx = length - idx
        # Use varible t to store the temporary string
        t = str_list[t_idx]
        # Start to move the bit
        str_list[t_idx] = str_list[idx]
        str_list[idx] = t
        idx += 1
    
    return ''.join(str_list)

方法3:

由 @Cotton 提供,直接用数组的方式,通过本用来做间隔数的段落进行反向遍历,我只能赞叹一句,Python 太强大了!

def reverse3(string):
    return string[::-1]

另一个是求素数,没想出什么比较好的算法,只能递归,但是因为除法(求余)非常缓慢,所以我对此算法非常不满。

方法1:

这里使用一个 range 直接生成一个数组,并用 for 递归,因为我觉得一次生成数组的速度应该比多次改写一个变量速度要快,但是自然,消耗内存稍大,如果对内存有要求,也可用 while 加递减代替。

def is_prime1(num):
    # Initial to presume it's a prime
    rt = True
    # It seems every number is possible to be the one, so it have to make a range.
    for i in range(2, num):
        if num % i == 0:
            rt = False
            break
    return rt

方法2:

jyf1987 提供,双数的判断一次解决(但是 3、5 的倍数如何能更快地排除呢?),剩下的是单数的递归,同时引入平方根(很好的想法,因为大于平方根后的数的计算已经没有意义,因为另一个数必然也是之前计算过的,因为 N = a*b,所以 N=M^2),降低运算量,引入 xrange 降低内存使用量,因为是原生实现,我相信应该不会比直接用 range 生成数组更慢。

from math import sqrt

def is_prime2(num):
    # Checking the Type/value
    if type(num) != int:
       raise TypeError

    if num < 2:
       raise ValueError('The number must be great than 1')

    # Inital to presume it's a prime
    rt = True
    sq_num = int(sqrt(num))

    # First we detect is it prime for 2
    if num == 2:
        return rt

    if num % 2 == 0:
        rt = False
        return rt
    
    # Now, start to detect the odd number
    # Because all of even number could division by 2, then how about 3? -_-#
    for i in xrange(3, sq_num, 2):
        if num % i == 0:
            rt = False
            break
    return rt

冰天雪地三十六度翻转跪求更好算法~嘿嘿。。。

Posted by K*K Thu, 09 Dec 2010 20:57:01 +0800


QPID 消息队列初步

QPID 是一种高性能 Message Bus,目前因为牵扯到工具的 SOA 架构,我的项目中将会整合它,以将自身对数据库的修改提交到 Message Bus 中, 供其它程序监听调用。

目前主流的 Message Bus 主要有以下几种:

  1. RabbitMQ
  2. Apache ActiveMQ
  3. Apache qpid

而之所以选择 QPID 是因为它有以下几个优点(引用源):

  • Supports transactions
  • Persistence using a pluggable layer — I believe the default is Apache Derby
  • This like the other Java based product is HIGHLY configurable
  • Management using JMX and an Eclipse Management Console application - http://www.lahiru.org/2008/08/what-qpid-management-console-can-do.html
  • The management console is very feature rich
  • Supports message Priorities
  • Automatic client failover using configurable connection properties -
    • http://qpid.apache.org/cluster-design-note.html
    • http://qpid.apache.org/starting-a-cluster.html
    • http://qpid.apache.org/cluster-failover-modes.html
  • Cluster is nothing but a set of machines have all the queues replicated
  • All queue data and metadata is replicated across all nodes that make up a cluster
  • All clients need to know in advance which nodes make up the cluster
  • Retry logic lies in the client code
  • Durable Queues/Subscriptions
  • Has bindings in many languages
  • For the curious: http://qpid.apache.org/current-architecture.html

而对我而言,QPID 比较有优势的地方是,一有 Python 的 bindding(Perl 的兄弟对不起了),二是源代码比较充足。

为此我写了两个基类,简单地调用了 QPID Python 中的 Receiver 和 Sender,相对于 message_transfer 方法,这种方法可以传递 Dictionary 对象,一共三个文件,其实也可以合在一起使用。

#!/usr/bin/python

import qpid
import qpid.messaging
import logging
logging.basicConfig()

class QPIDBase(object):
    def __init__(self, host='10.66.93.193', port='5672', queue_name='tmp.testing', username='guest', password='guest'):
        """
        Arguments:
            host
            port
            queue_name
            username
            password
        """
        self.host = host
        self.port = port
        self.queue_name = queue_name
        self.username = username
        self.password = password
        self.connection = None
        self.session = None
    
    def init_connect(self, mechanism='PLAIN'):
        """Initial the connection"""
        url = 'amqp://guest/guest@%s:%s' %(self.host, self.port)
        self.connection = qpid.messaging.Connection(
            url = url, sasl_mechanisms=mechanism,
            reconnect=True, reconnect_interval=60, reconnect_limit=60,
            username=self.username, password=self.password
        )
        self.connection.open()
        
    def init_session(self):
        """Initial the session"""
        if not self.connection:
            self.init_connect()
        self.session = self.connection.session()
    
    def close(self):
        """Close the connection and session"""
        self.session.close()
        self.connection.close()
#!/usr/bin/python

import qpid.messaging
from datetime import datetime
from base import QPIDBase

class QPIDSender(QPIDBase):
    def __init__(self, **kwargs):
        super(QPIDSender, self).__init__(**kwargs)
        self.sender = None
    
    def init_sender(self):
        """Initial the sender"""
        if not self.session:
            self.init_session()
        self.sender = self.session.sender(self.queue_name)
    
    def send(self, content, t = 'test'):
        """Sending the content"""
        if not self.sender:
            self.init_sender()
        props = {'type': t}
        message = qpid.messaging.Message(properties=props, content = content)
        self.sender.send(message)
    
    def typing(self):
        """Sending the contents real time with typing"""
        content = ''
        while content != 'EOF':
            content = raw_input('Start typing:')
            self.send(content)

if __name__ == '__main__':
    s = QPIDSender()
    s.send('Testing at %s' % datetime.now())
    s.close()
#!/usr/bin/python

from pprint import pprint
from base import QPIDBase

class QPIDReceiver(QPIDBase):
    def __init__(self, **kwargs):
        super(QPIDReceiver, self).__init__(**kwargs)
        self.receiver = None
    
    def init_receiver(self):
        """Initial the receiver"""
        if not self.session:
            self.init_session()
        self.receiver = self.session.receiver(self.queue_name)
        
    def receive(self):
        """Listing the messages from server"""
        if not self.receiver:
            self.init_receiver()
            
        try:
            while True:
                message = self.receiver.fetch()
                content = message.content
                pprint({'props': message.properties})
                pprint(content)
                self.session.acknowledge(message)
        except KeyboardInterrupt:
            pass
        
        self.close()

# Test code
if __name__ == '__main__':
    r = QPIDReceiver()
    r.receive()

代码非常简单,容易读懂,使用方法是在一台 Linux Server 上安装好 qpid-cpp-server, 并且启动后,在 Client 上安装 python-qpid,然后修改一下 base.py  __init__ 方法的 host 字段,或者在代码中自行指定好服务器地址,即可直接执行测试。

需要说明的是 QPID 返回的数据结构,包含可以为 Dictionary 对象的 properties 和只能为纯文本的 content 两个属性,也就是说可以将数据结构保存到 properties,而消息名称保存成 content 中,即:

        try:
            while True:
                message = self.receiver.fetch()
                content = message.content
                pprint({'props': message.properties})
                pprint(content)
                self.session.acknowledge(message)
        except KeyboardInterrupt:
            pass

一个终端执行 receiver.py 监听消息,再开一个终端执行 sender.py,将会如以下输出:

$ python ./receiver.py
{'props': {u'type': u'test', 'x-amqp-0-10.routing-key': u'tmp.testing'}}
'Testing at 2010-12-06 14:54:59.536093'

如果有兴趣试下 QPIDSender.typing() 方法,再把 Kerberos 的用户名读出来,就可以做一个 IM 啦~

问题:现在似乎 Sender 发出的消息一次只能有一个 Receiver 接收,也就是现有代码不能用于 SOA,而这理论上应该是不应该的,依然在探索。

(可以尝试打开两个 receiver.py 测试)

Posted by K*K Mon, 06 Dec 2010 23:03:21 +0800


Scrum 敏捷培训小节

敏捷里最重要的事情:

人、沟通、可用的软件、适应变化。

敏捷的特点:

  • Self-Organize
  • Cross-Functional
  • Practice
  • Learning
  • Transparency
  • Simplicity
  • Inspect & adapt
  • Iterative

3355 - 敏捷开发的基本构成:

3种角色:

  • CSM(Certified Scrum Master)
  • Product owner
  • Team

5 种特性:

  • Courage 勇气
  • Force 专注
  • Commitment 承诺
  • Openess 开放
  • Respect 尊重

Invest 模型:

  • Independent
  • Negotiable
  • Valuable
  • Estimatable
  • Small
  • Testable

PDCA:

  • Plan
  • Do
  • Check
  • Action

燃尽图:

在一个迭代内,将 Task 一点一点完成的线性图,正常情况下应该呈开始很平缓,中期急速下降,后期继续平缓的曲线,因为初期调查代码,寻找解决方案,Task 无法按量完成,中期快速完成 Task,后期快完成时速度减缓。不过这种情况不是绝对发生。

=====================

敏捷我的理解是,Product Owner 将用户的需求以 User Story 的方式写出来,由 Dev Team 分解成 Task,以自愿认领、自我估算开发时间的方式,以迭代的周期(1周太短,1月太长),以最终有价值的可用的软件为目标的开发方式。

它和传统开发模式的最大区别是,不再以需求为导向,也不用再写大量的需求文档,而将重点转移到了在每个迭代的周期结束时提供可用的软件为目标,换而言之,即不再以要求在特定时间内完成所有功能,而是通过短时间的迭代开发,快速交付可用的软件给用户,并及时得到反馈以决定下一个迭代的 Task。

敏捷的要求是 Product Owner 必须在每一个迭代的开始,公布 User Story,由 Team 拆分成 Task 并估算工作量,自愿认领 Task 并给与 Product Owner 承诺可以在这个迭代内完成,而这个迭代内,Product Owner 必须给予 Team 不被打扰的环境,和不会变化的 Task,尽一切力量在迭代结束时可以交付可用的软件,并在迭代结束时开启 Review 会议总结该迭代内的收获和教训,以及向 Customer 展示这个迭代内完成的功能。

敏捷对 Team 的要求是 Self-Organize 和 Cross-Fuctional,即“自组织”与“跨职能”,因为任务都是 Team 自行申请的,即可以做到自行规划开发计划,而“跨职能”则是指 Team member 能够主动去担当一些别的职位的工作,例如设计,测试等等。

而对项目的跟踪,也是通过 Task 的完成情况进行的,而具体表现,就是燃尽图。

我认为敏捷最大的优势有以下几个:

  1. 快速迭代,短期交付可用的软件给客户,可以及时得到反馈,如有问题,可以及时得到修改。
  2. Feature value,特性价值,对用户需求进行评估,优先完成最具有价值的特性。
  3. Task 自评估,自认领,自组织和跨职能,充分调动 Team 主人公精神,减少和 Product Manager 在认知上的矛盾,可以真正达到 6 倍速。
  4. Transparency,项目进度一目了然,工程问题容易暴露。
  5. Test driven 测试驱动开发,降低测试工作量,有效保证产品质量。

Posted by K*K Sun, 21 Nov 2010 06:24:05 +0800


Django 应用程序调试

 这里要介绍的是,全面的 Django app 调试,从最简单的打印变量,到使用 Django 自带的 Debug Middleware 调试 SQL,最后到全面的 Django debug toolbar 的使用。

1. 使用 pprint 打印变量(简单)

2. Django debug context processor[1](中级)

3. Django debug toolbar[2](更简单而强大。。。 -_-#)

一、使用 pprint 打印变量。

这是最简单的办法,在启动了 django-admin runserver 后,可将变量打印到终端上,适用于临时性的排错,当然还有其它办法,只是我觉得这种办法最简单。

下面是简单范例。

from pprint import pprint
from django.http import HttpResponse
from myapp.core.models import Case
def index(request, template = 'index.html'):
    c = Case.objects.select_related('author').get(pk = 100)
    pprint(str(c.query)) # 打印 C.objects.get(pk = 100) 调用的 SQL
    pprint(dict(c)) # 打印 C.objects.get(pk = 100) 的世纪内容。
    return HttpResponse(c.__dict__)

二、Django debug context processor[1]

该 Middleware 主要用于调试 SQL 执行情况,能够将所有的数据库查询 SQL 及花费时间打印出来,但是它要求代码使用 RequestContext,普通的 Context 和 render_to_response() 便无法直接使用了,如果之前代码使用 Context 构建,可能需要重写这部分代码。

其实我推荐在开始编写代码的时候,就使用 django.views.generic.simple.direct_to_template 来渲染页面,像如下: 

from django.views.generic.simple import direct_to_template    

def index(request, template = 'index.html'):
    ...
    return direct_to_template(request, template, {
        'parameters': parameters,
        'case': c,
    })

下面说安装和使用方法:

在 settings.py 的 'TEMPLATE_CONTEXT_PROCESSORS' 段中加入 'django.core.context_processors.debug',如下:

 

# RequestContext settings

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.request',
    'django.core.context_processors.media',
    'django.core.context_processors.debug',
    'myapp.core.context_processors.processor',
    ...
)

 

在 settings.py 中加入 'INTERNAL_IPS',用于识别开发机地址,内容写入本机 IP 地址即可:

 

# Needed by django.core.context_processors.debug:
# See http://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-debug

INTERNAL_IPS = ('127.0.0.1', )

 

然后,在共享模板的开头(别告诉我你一个页面一个模板文件。。。),加入生成 SQL 列表的代码:

 

<body id="body">
    	{% if debug %}
	<div id="debug">
		<p>
			{{ sql_queries|length }} Quer{{ sql_queries|pluralize:"y,ies" }}
			{% ifnotequal sql_queries|length 0 %}
			(<span style="cursor: pointer;" onclick="var s=document.getElementById('debugQueryTable').style;s.display=s.display=='none'?'':'none';this.innerHTML=this.innerHTML=='Show'?'Hide':'Show';">Show</span>)
			{% endifnotequal %}
		</p>
		<table id="debugQueryTable" style="display: none;">
			<tr class="odd">
				<td>#</td>
				<td>SQL</td>
				<td>Time</td>
			</tr>
			{% for query in sql_queries %}
			<tr class="{% cycle odd,even %}">
				<td>{{ forloop.counter }}</td>
				<td>{{ query.sql|escape }}</td>
				<td>{{ query.time }}</td>
			</tr>{% endfor %}
		</table>
	</div>
	{% endif %}
        ...
</body>

 

最终生成的效果是在页面顶部,增加了一个 XX Quueries 项,点击 (Show) 后如下:

Django debug context processor

三、Django debug toolbar[2]

Django debug toolbar 是我到目前为止见过的安装最简单,功能最强大的调试工具,它的主要特性有:

* 更加完善的 SQL 调试(比 Django debug processor 更加精准,Django debug contect processor 无法处理关系查询(Select related))

* 记录 CPU 使用时间(可惜没有针对代码级的 profile,希望未来的版本能增加这个功能)

* 完整记录 HTTP Headers 和 Request 请求

* 完整记录模板 Context 内容,包括 RequestContext 和直接传入的变量

* 记录 Signals

* python logging 模块的日志支持

安装也比较简单,可以使用 yum 直接安装,也从上面的地址下载后,直接使用 setuptools 通用的安装方法安装即可。

 

$ tar zxvf robhudson-django-debug-toolbar-7ba80e0.tar.gz
$ cd robhudson-django-debug-toolbar-7ba80e0
$ python ./setup.py build
$ sudo python ./setup.py install

 

如需确保安装正常,从 Python shell 里看看能否 import 即可,不出错,即安装正常:

 

$ python
Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import debug_toolbar
>>> 

 

然后是配置你的 settings.py。

我的调试 settings.py 和在产品服务器上运行的是不一样的,我也建议最好将二者分开,因为 Django app 开启 Debug 后对性能损耗非常严重。

将下面红字加入你自己的 settings.py 文件:

 

MIDDLEWARE_CLASSES = (
    ...
    'debug_toolbar.middleware.DebugToolbarMiddleware',
)

INSTALLED_APPS = (
    ...
    'debug_toolbar'
)

TEMPLATE_DIRS = (
    ...
    '/Library/Python/2.6/site-packages/django_debug_toolbar-0.8.3-py2.6.egg/debug_toolbar/templates/', #按需修改!指向 debug_toolbar 的模板目录。
)

DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.version.VersionDebugPanel',
'debug_toolbar.panels.timer.TimerDebugPanel',
'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
'debug_toolbar.panels.headers.HeaderDebugPanel',
'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
'debug_toolbar.panels.template.TemplateDebugPanel',
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel'
)

 

然后使用 django-admin.py runserver 启动测试服务器,下图是 Django debug toolbar 的 SQL 查询页面。

Django debug toolbar

相关链接:

[1] http://www.djangosnippets.org/snippets/93/

[2] http://github.com/robhudson/django-debug-toolbar/downloads

Posted by K*K Fri, 09 Apr 2010 22:15:31 +0800


Django 下的 Kerberos 登录

Kerberos 这种统一用户名和密码进行登录的方式在各大公司(尤其外企)内部应该都得到广泛应用,它以其安全、高效和易于管理等特性得到了很多系统管理员的喜爱。

目前网上对于 Kerberos 登录原理的描述都过于复杂,其实它的实现非常简单,当你向一个部署了 Kerberos 的应用服务器发起登录请求的时候,服务器会去 /etc/krb5.conf 里描述的 KDC 服务器用 Kerberos 协议发起一个登录请求,如果用户名密码验证通过,将会向服务器发一个票(Ticket),否则将会引出一个错误。然后服务器可以将票发给客户端,以后客户端就可以用这张票进行其它操作。与火车票和电影票一样,Kerberos 的票,也是有使用时间限制的,如果不经过特殊设置,这张票的超时时间大约是 6 个小时。

而在 Django 里使用 Kerberos 登录,有两种办法,一种是由 Django 直接向 KDC 验证密码,另一种是在 Apache 上使用 mod_auth_kerb 模块,由浏览器来处理登录请求。

这两种办法其实都是对 Django Auth Backend 的重载,所有的 Auth Backend 都位于 django/contrib/auth/backends.py 里,这里[2]也有一个使用 Email 来进行验证的范例,我受此启发,写了这两个例子,希望也能抛砖引玉,能给你们更多启发。

第一种 - 由 Django 直接向 KDC 验证密码:

这种办法比较简单,需要在 web server 上装好 python-kerberos 包,并且配置好 /etc/krb5.conf,详细的配置方法,最好咨询 IT 部门,配置成功后在服务器上用 Kerberos 上有的普通用户运行 kinit,如果能够密码验证通过就行。

并且在 Django 的 settings.py 里写入类似下面这行,定义 Kerberos 的 Realm:

# Kerberos settings
KRB5_REALM = 'EXAMPLE.COM'

与上面的 Email 验证例子类似的是,我们需要对 authenticate 方法进行重载,加入 kerberos 认证代码,python-kerberos 已经提供了 checkPassword 方法。 

try:
    auth = kerberos.checkPassword(
        username, password, '',
        settings.KRB5_REALM
    )
except kerberos.BasicAuthError, e:
    return None

完整代码如下:

import kerberos
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User

class KerberosBackend(ModelBackend):
    """
    Kerberos authorization backend for TCMS.
    
    Required python-kerberos backend, correct /etc/krb5.conf file,
    And correct KRB5_REALM settings in settings.py.
    
    Example in settings.py:
    # Kerberos settings
    KRB5_REALM = 'EXAMPLE.COM'
    """
    def authenticate(self, username=None, password=None):
        try:
            auth = kerberos.checkPassword(
                username, password, '',
                settings.KRB5_REALM
            )
        except kerberos.BasicAuthError, e:
            return None
        
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = User.objects.create_user(
                username = username,
                email = '%s@%s' % (username, settings.KRB5_REALM.lower())
            )
        
        user.set_unusable_password()
        user.save()
        return user

 

第二种:Apache 上使用 mod_auth_kerb:

这种方法略有复杂,部署它需要向 KDC 申请一个 keytab 文件,以授权该 Web Server 向  KDC 发起请求,并且需要安装和配置 mod_auth_kerb(很简单,后面有),并且 /etc/krb5.conf 一点也不能少。

但是好处也是非常明显的,上面那种依然是使用 Django Auth Contrib 的 Session Manager 来负责登录信息的维持,但是这种方法将能够完全使用 Kerberos 自身提供的 Features,包括登录维持,和 kinit 的支持,也就是说,只要在本机上使用 kinit 成功登录过一次,用 Firefox (目前似乎在 Linux 上只支持该浏览器)访问部署了 mod_auth_kerb 的网站,将都不再需要登录。

它的原理包括两种条件,一种是没有在本机执行 kinit 的,使用 Firefox 直接访问服务器,服务器将会返回一个 401 Authorization Required 错误,这时 Firefox 会弹出对话框询问你的 Kerberos 用户名和密码,并提交你的密码。另一种在本机已经执行过 kinit 的,Firefox 会去读取你客户端的 Kerberos ticket 缓存,如果没有过期的话,就会使用它。无论哪种办法,Firefox 都将在 HTTP Header 里添加一个 'Authorization' 段,并且加入 Basic Authorization 验证方式,例如我本机上的:

Authorization	Basic eGt1YW5nOkxvdmVvZnJvYWQuMTIz

使用这种部署方式,在 Django 1.1 版本以下,还没有比较好的解决办法,但好在 Django 1.1 提供了 RemoteUserBackend 后端,依然在 django/contrib/auth/backends.py 路径里,通过阅读它的代码,我们可以看到它其实依然是个 ModelBackend 的继承,而 Django 的 Request Handler 已经默认将 HTTP Meta 里的 REMOTE_USER 段给加入处理范围之内了,因此 RemoteUserBackend 的 ’authenticate‘ 与 ModelBackend 不太一样。 :-)

其实代码都已经写好,我们只需要处理一下拿到用户后的处理办法('configure_user' 方法)和处理用户名的方法('clean_username' 方法)就可以了。

我这里在拿到用户后,出于保护密码的原则,为该用户设置了一个无效密码('user.set_unusable_password()' 方法),并且设置了该用户的 Email。 同时,因为 RemoteUserBackend 默认返回的用户名是 ‘[username]@[KRB5_REALM]',所以我也把后面的 REALM 给去掉,直接贴代码:

from django.conf import settings
from django.contrib.auth.backends import RemoteUserBackend

class ModAuthKerbBackend(RemoteUserBackend):
    """
    mod_auth_kerb modules authorization backend for TCMS.
    Based on DjangoRemoteUser backend.
    
    Required correct /etc/krb5.conf, /etc/krb5.keytab and
    Correct mod_auth_krb5 module settings for apache.
    
    Example apache settings:
    
    # Set a httpd config to protect krb5login page with kerberos.
    # You need to have mod_auth_kerb installed to use kerberos auth.
    # Httpd config /etc/httpd/conf.d/<project>.conf should look like this:
    
    <Location "/">

        SetHandler python-program
        PythonHandler django.core.handlers.modpython
        SetEnv DJANGO_SETTINGS_MODULE <project>.settings
        PythonDebug On
    </Location>
    
    <Location "/auth/krb5login">
        AuthType Kerberos
        AuthName "<project> Kerberos Authentication"
        KrbMethodNegotiate on
        KrbMethodK5Passwd off
        KrbServiceName HTTP
        KrbAuthRealms EXAMPLE.COM
        Krb5Keytab /etc/httpd/conf/http.<hostname>.keytab
        KrbSaveCredentials off
        Require valid-user
    </Location>

    """
    def configure_user(self, user):
        """
        Configures a user after creation and returns the updated user.
        
        By default, returns the user unmodified.
        Here, the user will changed to a unusable password
        and set the email.
        """
        user.email = user.username + '@' + settings.KRB5_REALM.lower()
        user.set_unusable_password()
        user.save()
        return user
    
    def clean_username(self, username):
        """
        Performs any cleaning on the "username" prior to using it to get or
        create the user object.  Returns the cleaned username.
        
        For more info, reference clean_username function in 
        django/auth/backends.py
        """
        return username.replace('@' + settings.KRB5_REALM, '')

Django 是一个很强大的框架,虽然缺点和优点都同样的明显,有些甚至是由于 Python 语言或者类库造成的问题,但是因为其使用的便利性,高效的开发,而且其开发小组也非常活跃,使其特性的添加非常频繁,而且网上也有大量资源,例如 Django Snippets 网站,因此依然有着非常巨大优势。而通过阅读它的代码,往往都能获得更多启发。

链接:

[1] http://trac.calendarserver.org/browser/PyKerberos/

[2] http://www.djangosnippets.org/snippets/74/

Posted by K*K Fri, 26 Feb 2010 19:19:34 +0800