1. 關於sessionhtml
flask session可能不少人根本都沒有使用過,卻是cookie你們可能使用得比較多。flask cookie使用起來比較簡單,就兩個函數,讀取和設置。python
具體使用方式以下:sql
讀取cookie數據庫
from flask import request @app.route('/') def index(): username = request.cookies.get('username') # 使用 cookies.get(key) 來代替 cookies[key] , # 以免當 cookie 不存在時引起 KeyError 。
設置cookieflask
@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。這塊就不細講了。cookie
這節主要講的是session,具體cookie和session的區別,在網上能夠查找到不少相關的資料。其實不少事情不須要刻意理解他們之間的具體差異,實際中遇到就能很好地理解他們之間的差異。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 小結
以前關於這個問題想了很久,沒有想到具體的解決方案,後來無心間想到了。因此,遇到問題不要放棄,堅持就是勝利!