[server/pyro] try to shutdown the repository properly
If RepositoryServer.trigger_events is not called we might miss a
QuitEvent and thus never shutdown the repository and its looping tasks.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""Core hooks: check for data integrity according to the instance'schemavalidity"""__docformat__="restructuredtext en"_=unicodefromthreadingimportLockfromcubicwebimportvalidation_errorfromcubicweb.schemaimport(META_RTYPES,WORKFLOW_RTYPES,RQLConstraint,RQLUniqueConstraint)fromcubicweb.predicatesimportis_instancefromcubicweb.uilibimportsoup2xhtmlfromcubicweb.serverimporthook# special relations that don't have to be checked for integrity, usually# because they are handled internally by hooks (so we trust ourselves)DONT_CHECK_RTYPES_ON_ADD=META_RTYPES|WORKFLOW_RTYPESDONT_CHECK_RTYPES_ON_DEL=META_RTYPES|WORKFLOW_RTYPES_UNIQUE_CONSTRAINTS_LOCK=Lock()_UNIQUE_CONSTRAINTS_HOLDER=Nonedef_acquire_unique_cstr_lock(session):"""acquire the _UNIQUE_CONSTRAINTS_LOCK for the session. This lock used to avoid potential integrity pb when checking RQLUniqueConstraint in two different transactions, as explained in http://intranet.logilab.fr/jpl/ticket/36564 """if'uniquecstrholder'insession.transaction_data:return_UNIQUE_CONSTRAINTS_LOCK.acquire()session.transaction_data['uniquecstrholder']=True# register operation responsible to release the lock on commit/rollback_ReleaseUniqueConstraintsOperation(session)def_release_unique_cstr_lock(session):if'uniquecstrholder'insession.transaction_data:delsession.transaction_data['uniquecstrholder']_UNIQUE_CONSTRAINTS_LOCK.release()class_ReleaseUniqueConstraintsOperation(hook.Operation):defpostcommit_event(self):_release_unique_cstr_lock(self.session)defrollback_event(self):_release_unique_cstr_lock(self.session)class_CheckRequiredRelationOperation(hook.DataOperationMixIn,hook.LateOperation):"""checking relation cardinality has to be done after commit in case the relation is being replaced """containercls=listrole=key=base_rql=Nonedefprecommit_event(self):session=self.sessionpendingeids=session.transaction_data.get('pendingeids',())pendingrtypes=session.transaction_data.get('pendingrtypes',())foreid,rtypeinself.get_data():# recheck pending eids / relation typesifeidinpendingeids:continueifrtypeinpendingrtypes:continueifnotsession.execute(self.base_rql%rtype,{'x':eid}):etype=session.describe(eid)[0]msg=_('at least one relation %(rtype)s is required on ''%(etype)s (%(eid)s)')raisevalidation_error(eid,{(rtype,self.role):msg},{'rtype':rtype,'etype':etype,'eid':eid},['rtype','etype'])class_CheckSRelationOp(_CheckRequiredRelationOperation):"""check required subject relation"""role='subject'base_rql='Any O WHERE S eid %%(x)s, S %s O'class_CheckORelationOp(_CheckRequiredRelationOperation):"""check required object relation"""role='object'base_rql='Any S WHERE O eid %%(x)s, S %s O'classIntegrityHook(hook.Hook):__abstract__=Truecategory='integrity'classCheckCardinalityHookBeforeDeleteRelation(IntegrityHook):"""check cardinalities are satisfied"""__regid__='checkcard_before_delete_relation'events=('before_delete_relation',)def__call__(self):rtype=self.rtypeifrtypeinDONT_CHECK_RTYPES_ON_DEL:returnsession=self._cweidfrom,eidto=self.eidfrom,self.eidtordef=session.rtype_eids_rdef(rtype,eidfrom,eidto)if(rdef.subject,rtype,rdef.object)insession.transaction_data.get('pendingrdefs',()):returncard=rdef.cardinalityifcard[0]in'1+'andnotsession.deleted_in_transaction(eidfrom):_CheckSRelationOp.get_instance(session).add_data((eidfrom,rtype))ifcard[1]in'1+'andnotsession.deleted_in_transaction(eidto):_CheckORelationOp.get_instance(session).add_data((eidto,rtype))classCheckCardinalityHookAfterAddEntity(IntegrityHook):"""check cardinalities are satisfied"""__regid__='checkcard_after_add_entity'events=('after_add_entity',)def__call__(self):eid=self.entity.eideschema=self.entity.e_schemaforrschema,targetschemas,roleineschema.relation_definitions():# skip automatically handled relationsifrschema.typeinDONT_CHECK_RTYPES_ON_ADD:continuerdef=rschema.role_rdef(eschema,targetschemas[0],role)ifrdef.role_cardinality(role)in'1+':ifrole=='subject':op=_CheckSRelationOp.get_instance(self._cw)else:op=_CheckORelationOp.get_instance(self._cw)op.add_data((eid,rschema.type))class_CheckConstraintsOp(hook.DataOperationMixIn,hook.LateOperation):""" check a new relation satisfy its constraints """containercls=listdefprecommit_event(self):session=self.sessionforvaluesinself.get_data():eidfrom,rtype,eidto,constraints=values# first check related entities have not been deleted in the same# transactionifsession.deleted_in_transaction(eidfrom):continueifsession.deleted_in_transaction(eidto):continueforconstraintinconstraints:# XXX# * lock RQLConstraint as well?# * use a constraint id to use per constraint lock and avoid# unnecessary commit serialization ?ifisinstance(constraint,RQLUniqueConstraint):_acquire_unique_cstr_lock(session)try:constraint.repo_check(session,eidfrom,rtype,eidto)exceptNotImplementedError:self.critical('can\'t check constraint %s, not supported',constraint)classCheckConstraintHook(IntegrityHook):"""check the relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkconstraint'events=('after_add_relation',)def__call__(self):# XXX get only RQL[Unique]Constraints?rdef=self._cw.rtype_eids_rdef(self.rtype,self.eidfrom,self.eidto)constraints=rdef.constraintsifconstraints:_CheckConstraintsOp.get_instance(self._cw).add_data((self.eidfrom,self.rtype,self.eidto,constraints))classCheckAttributeConstraintHook(IntegrityHook):"""check the attribute relation satisfy its constraints this is delayed to a precommit time operation since other relation which will make constraint satisfied (or unsatisfied) may be added later. """__regid__='checkattrconstraint'events=('after_add_entity','after_update_entity')def__call__(self):eschema=self.entity.e_schemaforattrinself.entity.cw_edited:ifeschema.subjrels[attr].final:constraints=[cforcineschema.rdef(attr).constraintsifisinstance(c,(RQLUniqueConstraint,RQLConstraint))]ifconstraints:_CheckConstraintsOp.get_instance(self._cw).add_data((self.entity.eid,attr,None,constraints))classCheckUniqueHook(IntegrityHook):__regid__='checkunique'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entityeschema=entity.e_schemaforattr,valinentity.cw_edited.iteritems():ifeschema.subjrels[attr].finalandeschema.has_unique_values(attr):ifvalisNone:continuerql='%s X WHERE X %s%%(val)s'%(entity.e_schema,attr)rset=self._cw.execute(rql,{'val':val})ifrsetandrset[0][0]!=entity.eid:msg=_('the value "%s" is already used, use another one')raisevalidation_error(entity,{(attr,'subject'):msg},(val,))classDontRemoveOwnersGroupHook(IntegrityHook):"""delete the composed of a composite relation when this relation is deleted """__regid__='checkownersgroup'__select__=IntegrityHook.__select__&is_instance('CWGroup')events=('before_delete_entity','before_update_entity')def__call__(self):entity=self.entityifself.event=='before_delete_entity'andentity.name=='owners':raisevalidation_error(entity,{None:_("can't be deleted")})elifself.event=='before_update_entity' \and'name'inentity.cw_edited:oldname,newname=entity.cw_edited.oldnewvalue('name')ifoldname=='owners'andnewname!=oldname:raisevalidation_error(entity,{('name','subject'):_("can't be changed")})classTidyHtmlFields(IntegrityHook):"""tidy HTML in rich text strings"""__regid__='htmltidy'events=('before_add_entity','before_update_entity')def__call__(self):entity=self.entitymetaattrs=entity.e_schema.meta_attributes()edited=entity.cw_editedformetaattr,(metadata,attr)inmetaattrs.iteritems():ifmetadata=='format'andattrinedited:try:value=edited[attr]exceptKeyError:continue# no text to tidyifisinstance(value,unicode):# filter out None and Binaryifgetattr(entity,str(metaattr))=='text/html':edited[attr]=soup2xhtml(value,self._cw.encoding)classStripCWUserLoginHook(IntegrityHook):"""ensure user logins are stripped"""__regid__='stripuserlogin'__select__=IntegrityHook.__select__&is_instance('CWUser')events=('before_add_entity','before_update_entity',)def__call__(self):login=self.entity.cw_edited.get('login')iflogin:self.entity.cw_edited['login']=login.strip()# 'active' integrity hooks: you usually don't want to deactivate them, they are# not really integrity check, they maintain consistency on changesclass_DelayedDeleteOp(hook.DataOperationMixIn,hook.Operation):"""delete the object of composite relation except if the relation has actually been redirected to another composite """base_rql=Nonedefprecommit_event(self):session=self.sessionpendingeids=session.transaction_data.get('pendingeids',())eids_by_etype_rtype={}foreid,rtypeinself.get_data():# don't do anything if the entity is being deletedifeidnotinpendingeids:etype=session.describe(eid)[0]key=(etype,rtype)ifkeynotineids_by_etype_rtype:eids_by_etype_rtype[key]=[str(eid)]else:eids_by_etype_rtype[key].append(str(eid))for(etype,rtype),eidsineids_by_etype_rtype.iteritems():# quite unexpectedly, not deleting too many entities at a time in# this operation benefits to the exec speed (possibly on the RQL# parsing side)start=0incr=500whilestart<len(eids):session.execute(self.base_rql%(etype,','.join(eids[start:start+incr]),rtype))start+=incrclass_DelayedDeleteSEntityOp(_DelayedDeleteOp):"""delete orphan subject entity of a composite relation"""base_rql='DELETE %s X WHERE X eid IN (%s), NOT X %s Y'class_DelayedDeleteOEntityOp(_DelayedDeleteOp):"""check required object relation"""base_rql='DELETE %s X WHERE X eid IN (%s), NOT Y %s X'classDeleteCompositeOrphanHook(hook.Hook):"""delete the composed of a composite relation when this relation is deleted """__regid__='deletecomposite'events=('before_delete_relation',)category='activeintegrity'def__call__(self):# if the relation is being delete, don't delete composite's components# automaticallysession=self._cwrtype=self.rtyperdef=session.rtype_eids_rdef(rtype,self.eidfrom,self.eidto)if(rdef.subject,rtype,rdef.object)insession.transaction_data.get('pendingrdefs',()):returncomposite=rdef.compositeifcomposite=='subject':_DelayedDeleteOEntityOp.get_instance(self._cw).add_data((self.eidto,rtype))elifcomposite=='object':_DelayedDeleteSEntityOp.get_instance(self._cw).add_data((self.eidfrom,rtype))