1. 關於session
flask session可能很多人根本都沒有使用過,倒是cookie大家可能使用得比較多。flask cookie使用起來比較簡單,就兩個函數,讀取和設置。
具體使用方式如下:
讀取cookie
from flask import request
@app.route('/')
def index():
username = request.cookies.get('username')
# 使用 cookies.get(key) 來代替 cookies[key] ,
# 以避免當 cookie 不存在時引發 KeyError 。
設置cookie
@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
cookie的設置是必須在response返回時,並且是作為response的一部分給client段返回的。
其實理解了cookie的原理就大概知道為什么這么做了,cookie是為了保存用戶的一些信息,而這些信息其實是保存在瀏覽器的緩存中的,大家平時清理瀏覽器的訪問記錄時其實就是在清除對應的cookie。
每次請求的時候,瀏覽器會根據所訪問的網頁把對應保存的用戶相關的cookie信息放到request中給服務端發送過去。這樣就節省了多次身份認證的過程。
關於flask cookie的詳細使用方法可以參閱:http://dormousehole.readthedocs.io/en/latest/quickstart.html?highlight=cookie。這塊就不細講了。
這節主要講的是session,具體cookie和session的區別,在網上可以查找到很多相關的資料。其實很多事情不需要刻意理解他們之間的具體差別,實際中遇到就能很好地理解他們之間的差別。
本文不會具體講cookie和session概念上的差別,不過通過介紹完我使用的場景,想必大家就很清楚他們之間的區別了。
2 遇到的問題
自動化發布平台在用戶創建工單表單的時候,需要用戶指定其待發布的機器列表。這樣OP在具體發布的時候根據發布所處的階段來選擇具體發布哪幾台機器。因此,需要根據用戶對應的業務從配置服務中拉取對應的機器列表讓其選擇。
並且這個機器列表是可能變化的。所以,這塊單純使用wtforms.fields.SelectField表單字段是不起作用的,因此需要使用wtforms.ext.sqlalchemy.fields的QuerySelectField字段。至於QuerySelectField的使用可以參考我的
上篇博客。
OP在發布的不同階段,需要選定機器列表做具體的發布。具體機器列表的獲取,也可以使用和創建工單的時候同樣的方式,從配置服務中獲取當前業務所有的機器列表。然后,讓發布者從機器列表中選擇機器列表中選擇幾台具體的機器做發布。
但是,這塊會有一個問題。創建工單的時候,需要創建者從一個機器列表中選擇幾台期待的機器,而發布的各個階段也都需要從同樣的機器列表中選擇幾台機器做發布。這樣就可能存在一個問題,發布者發布的時候應該是從創建工單的人選擇的
機器列表中選擇機器做發布,而不是從全局的機器列表中選擇機器做發布。
因此,需要有種方式在創建表單的時候能夠把工單的ID傳遞過去,這樣可以從數據庫中把工單的詳細信息查詢出來,並獲取到用戶選定的機器列表。
但是,這塊用什么方式傳遞過去呢?
可能這樣說不是很形象,具體用代碼展示吧,發布過程中有預發布、灰度發布、以及發布。但是,這塊以預發布為例,灰度發布和發布其實都一樣。
@expose('/pre_release', methods = ['GET', 'POST'])
def pre_release(self):
pre_release_handle_form = PreReleaseHandleForm(request.form)
if request.method == 'POST':
if helpers.validate_form_on_submit(pre_release_handle_form):
# user logic
return self.render('release.html', form = pre_release_handle_form
上面是預發布相關的邏輯,核心邏輯都去掉了,只留個大致的框架,不過這並不影響本文介紹的內容。
而PreReleaseHandleForm表結構如下:
class PreReleaseHandleForm(form.Form):
def query_factory():
return [r.task_name for r in db.session.query(Task).all()]
def get_pk(obj):
return obj
def iplist_query_factory():
# 從配置服務中獲取
iplist = get_from_config_server()
return iplist
def iplist_get_pk(obj):
return obj
release_task = QuerySelectField(label=u'發布任務',
validators=[validators.required()],
query_factory=query_factory,
get_pk=get_pk)
rollback_task = QuerySelectField(label=u'回滾任務',
validators=[validators.required()],
query_factory=query_factory,
get_pk=get_pk)
target_machine = QuerySelectMultipleField(label=u'目標機器',
validators = [validators.required()],
query_factory=iplist_query_factory,
get_pk=iplist_get_pk)
remark = fields.TextField(label=u'備注', validators=[validators.required()])
此處可以看到QuerySelectMultipleField函數,其能夠支持表單的多選以及能夠動態獲取選項列表。因此,在此處替代MultiSelectField。
上述的代碼不做詳細介紹,算是比較簡單的使用,我們關注的核心是iplist_query_factory函數,它是本文的核心。預發布的機器列表就是由此函數產生的。
但是,由於我們在預發布的時候表單並沒有上下文,我們不知道其對應哪個具體的工單,而且表單肯定是無狀態的。所以,就跟此處有沖突,我們期待在表單頁面顯示的時候就能夠
根據具體的上下文從創建的工單中獲取待發布的機器列表,並展示出來。
3 解決的辦法
因此,第一個想法是使用cookie,這樣我們在向客戶端顯示表單頁面的時候順便把工單ID放個response中並給客戶端返回。這樣iplist_query_factory函數會根據request上下文從cookie中
取出對應的ID,並從數據庫中查詢對應的工單詳細信息,並回去對應的待發布機器列表並給客戶端展示。
不過這塊需要PreReleaseHandleForm和pre_release函數都做稍微的修改,具體修改內容如下:
def iplist_query_factory():
id = request.cookie.get('ID')
job = db.session.query(Job).filter_by(id = id).first()
iplist = job.pre_release_iplist.split(',')
return iplist
@expose('/pre_release', methods = ['GET', 'POST'])
def pre_release(self):
id = request.args.get('id', '')
pre_release_handle_form = PreReleaseHandleForm(request.form)
if request.method == 'POST':
if helpers.validate_form_on_submit(pre_release_handle_form):
# user logic
resp = make_response(self.render('release.html', form = pre_release_handle_form))
resp.set_cookie('ID', id)
return resp
具體的話,大家可以關注上面加粗的地方。其實還是蠻容易理解的,在向客戶端返回response的時候需要在response的cookie屬性中設置對應的ID字段的值,在iplist_query_factory中從cookie中查詢對應字段的值。
這塊並不會因為多線程而出現問題,因為在flask中request上下文是線程安全的。
但是測試的時候發現這塊有個問題,就是我第一次展示表單頁面的時候IP列表是空的,必須我重新刷新一次才可以。之后的情況就是我重新打開頁面,顯示的機器列表確實上一次的機器列表。
后來想了想,才發現一個問題。每次的表單顯示的時候其實對應的response並沒有給客戶端返回,因此拿到的是上次請求的cookie值,這樣就存在着延后,跟真實的情況不同步。
要是任由這種情況發展還是蠻危險的。因此,只能使用另外一種方式解決了,后來發現另外一種解決方式就是session。
看了下flask admin session的具體實現,發現flask session其實是request上下文,而flask的實現其實對於每個request會分派給一個獨立的線程,其實是線程安全的。
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request context')
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
上面是flask源代碼,具體只需要看下上面加粗的代碼就可以了。可以看出session其實是request上下文安全的。
因此,可以直接使用。至於fllask session的使用也是蠻簡單的。具體代碼如下:
def iplist_query_factory():
id = session['ID']
job = db.session.query(Job).filter_by(id = id).first()
iplist = job.pre_release_iplist.split(',')
return iplist
@expose('/pre_release', methods = ['GET', 'POST'])
def pre_release(self):
id = request.args.get('id', '')
session['ID'] = id
pre_release_handle_form = PreReleaseHandleForm(request.form)
if request.method == 'POST':
if helpers.validate_form_on_submit(pre_release_handle_form):
# user logic
returnn self.render('release.html', form = pre_release_handle_form)
上面加粗的代碼就是session的使用,發現沒有其實session使用起來比cookie更方便。
測試發現效果完全符合預期,但是蠻不錯的。
4 小結
之前關於這個問題想了好久,沒有想到具體的解決方案,后來無意間想到了。所以,遇到問題不要放棄,堅持就是勝利!
