mirror of
https://github.com/OCA/report-print-send.git
synced 2025-02-16 07:11:31 +02:00
Use a cron instead of threads to update printers status
The implementation with threads was blocking the loading of the server in multiprocess. Using a cron will lower the frequency of the updates but at least it is simple and reliable. Fixes #14
This commit is contained in:
@@ -24,195 +24,14 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from datetime import datetime
|
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import cups
|
import cups
|
||||||
import psycopg2
|
|
||||||
|
|
||||||
from openerp import models, fields, api, sql_db
|
from openerp import models, fields, api
|
||||||
from openerp.tools import ormcache
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
POLL_INTERVAL = 15 # seconds
|
|
||||||
|
|
||||||
|
|
||||||
class PrintingPrinterPolling(models.Model):
|
|
||||||
""" Keep the last update time of printers update.
|
|
||||||
|
|
||||||
This table will contain only 1 row, with the last time we checked
|
|
||||||
the list of printers from cups.
|
|
||||||
|
|
||||||
The table is locked before an update so 2 processes won't be able
|
|
||||||
to do the update at the same time.
|
|
||||||
"""
|
|
||||||
_name = 'printing.printer.polling'
|
|
||||||
_description = 'Printers Polling'
|
|
||||||
|
|
||||||
last_update = fields.Datetime()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def find_unique_record(self):
|
|
||||||
polling = self.search([], limit=1)
|
|
||||||
return polling
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
@ormcache()
|
|
||||||
def table_exists(self):
|
|
||||||
return self._model._table_exist(self.env.cr)
|
|
||||||
|
|
||||||
def _create_table(self, cr):
|
|
||||||
super(PrintingPrinterPolling, self)._create_table(cr)
|
|
||||||
self.clear_caches()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def find_or_create_unique_record(self):
|
|
||||||
polling = self.find_unique_record()
|
|
||||||
if polling:
|
|
||||||
return polling
|
|
||||||
cr = self.env.cr
|
|
||||||
try:
|
|
||||||
# Will be released at the end of the transaction. Locks the
|
|
||||||
# full table for insert/update because we must have only 1
|
|
||||||
# record in this table, so we prevent 2 processes to create
|
|
||||||
# each one one line at the same time.
|
|
||||||
cr.execute("LOCK TABLE %s IN SHARE ROW EXCLUSIVE MODE NOWAIT" %
|
|
||||||
self._table, log_exceptions=False)
|
|
||||||
except psycopg2.OperationalError as err:
|
|
||||||
# the lock could not be acquired, already running
|
|
||||||
if err.pgcode == '55P03':
|
|
||||||
_logger.debug('Another process/thread is already '
|
|
||||||
'creating the polling record.')
|
|
||||||
return self.browse()
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return self.create({'last_update': False})
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def lock(self):
|
|
||||||
""" Lock the polling record
|
|
||||||
|
|
||||||
Lock the record in the database so we can prevent concurrent
|
|
||||||
processes to update at the same time.
|
|
||||||
|
|
||||||
The lock is released either on commit or rollback of the
|
|
||||||
transaction.
|
|
||||||
|
|
||||||
Returns if the record has been locked or not.
|
|
||||||
"""
|
|
||||||
self.ensure_one()
|
|
||||||
cr = self.env.cr
|
|
||||||
sql = ("SELECT id FROM %s WHERE id = %%s FOR UPDATE NOWAIT" %
|
|
||||||
self._table)
|
|
||||||
try:
|
|
||||||
cr.execute(sql, (self.id, ), log_exceptions=False)
|
|
||||||
except psycopg2.OperationalError as err:
|
|
||||||
# the lock could not be acquired, already running
|
|
||||||
if err.pgcode == '55P03':
|
|
||||||
_logger.debug('Another process/thread is already '
|
|
||||||
'updating the printers list.')
|
|
||||||
return False
|
|
||||||
if err.pgcode == '40001':
|
|
||||||
_logger.debug('could not serialize access due to '
|
|
||||||
'concurrent update')
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return True
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
@api.model
|
|
||||||
def start_update(self):
|
|
||||||
locked = False
|
|
||||||
polling = self.find_or_create_unique_record()
|
|
||||||
if polling:
|
|
||||||
if polling.lock():
|
|
||||||
locked = True
|
|
||||||
yield locked
|
|
||||||
if locked:
|
|
||||||
polling.write({'last_update': fields.Datetime.now()})
|
|
||||||
|
|
||||||
@ormcache()
|
|
||||||
def _last_update_cached(self):
|
|
||||||
""" Get the last update's datetime, the returned value is cached """
|
|
||||||
polling = self.find_unique_record()
|
|
||||||
if not polling:
|
|
||||||
return False
|
|
||||||
last_update = polling.last_update
|
|
||||||
if last_update:
|
|
||||||
last_update = fields.Datetime.from_string(last_update)
|
|
||||||
return last_update
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def last_update_cached(self):
|
|
||||||
""" Returns the last update datetime from a cache
|
|
||||||
|
|
||||||
The check if the list of printers needs to be refreshed is
|
|
||||||
called very often (each time a browse is done on ``res.users``),
|
|
||||||
so we avoid to hit the database on every updates by keeping the
|
|
||||||
last value in cache.
|
|
||||||
The cache has no expiration so we manually clear it when the
|
|
||||||
poll interval (defaulted to 10 seconds) is reached.
|
|
||||||
"""
|
|
||||||
last_update = self._last_update_cached()
|
|
||||||
now = datetime.now()
|
|
||||||
if last_update and (now - last_update).seconds >= POLL_INTERVAL:
|
|
||||||
# Invalidates last_update_cached and read a fresh value
|
|
||||||
# from the database
|
|
||||||
self.clear_caches()
|
|
||||||
return self._last_update_cached()
|
|
||||||
return last_update
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def need_update(self):
|
|
||||||
last_update = self.last_update_cached()
|
|
||||||
now = datetime.now()
|
|
||||||
# Only update printer status if current status is more than 10
|
|
||||||
# seconds old.
|
|
||||||
if not last_update or (now - last_update).seconds >= POLL_INTERVAL:
|
|
||||||
self.clear_caches() # invalidates last_update_cached
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def update_printers_status(self):
|
|
||||||
cr = sql_db.db_connect(self.env.cr.dbname).cursor()
|
|
||||||
uid, context = self.env.uid, self.env.context
|
|
||||||
with api.Environment.manage():
|
|
||||||
try:
|
|
||||||
self.env = api.Environment(cr, uid, context)
|
|
||||||
printer_obj = self.env['printing.printer']
|
|
||||||
with self.start_update() as locked:
|
|
||||||
if not locked:
|
|
||||||
return # could not obtain lock
|
|
||||||
|
|
||||||
printer_recs = printer_obj.search([])
|
|
||||||
|
|
||||||
try:
|
|
||||||
connection = cups.Connection()
|
|
||||||
printers = connection.getPrinters()
|
|
||||||
except:
|
|
||||||
printer_recs.write({'status': 'server-error'})
|
|
||||||
else:
|
|
||||||
for printer in printer_recs:
|
|
||||||
cups_printer = printers.get(printer.system_name)
|
|
||||||
if cups_printer:
|
|
||||||
printer.update_from_cups(connection,
|
|
||||||
cups_printer)
|
|
||||||
else:
|
|
||||||
# not in cups list
|
|
||||||
printer.status = 'unavailable'
|
|
||||||
|
|
||||||
self.env.cr.commit()
|
|
||||||
except:
|
|
||||||
self.env.cr.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.env.cr.close()
|
|
||||||
|
|
||||||
|
|
||||||
class PrintingPrinter(models.Model):
|
class PrintingPrinter(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -240,6 +59,24 @@ class PrintingPrinter(models.Model):
|
|||||||
location = fields.Char(readonly=True)
|
location = fields.Char(readonly=True)
|
||||||
uri = fields.Char(string='URI', readonly=True)
|
uri = fields.Char(string='URI', readonly=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def update_printers_status(self):
|
||||||
|
printer_recs = self.search([])
|
||||||
|
try:
|
||||||
|
connection = cups.Connection()
|
||||||
|
printers = connection.getPrinters()
|
||||||
|
except:
|
||||||
|
printer_recs.write({'status': 'server-error'})
|
||||||
|
else:
|
||||||
|
for printer in printer_recs:
|
||||||
|
cups_printer = printers.get(printer.system_name)
|
||||||
|
if cups_printer:
|
||||||
|
printer.update_from_cups(connection, cups_printer)
|
||||||
|
else:
|
||||||
|
# not in cups list
|
||||||
|
printer.status = 'unavailable'
|
||||||
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _prepare_update_from_cups(self, cups_connection, cups_printer):
|
def _prepare_update_from_cups(self, cups_connection, cups_printer):
|
||||||
mapping = {
|
mapping = {
|
||||||
@@ -299,40 +136,6 @@ class PrintingPrinter(models.Model):
|
|||||||
_logger.info("Printing job: '%s'" % file_name)
|
_logger.info("Printing job: '%s'" % file_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.model
|
|
||||||
def start_printer_update(self):
|
|
||||||
polling_obj = self.env['printing.printer.polling']
|
|
||||||
thread = Thread(target=polling_obj.update_printers_status, args=())
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def update(self):
|
|
||||||
"""Update printer status if current status is more than 10s old."""
|
|
||||||
polling_obj = self.env['printing.printer.polling']
|
|
||||||
if not polling_obj.table_exists():
|
|
||||||
# On the installation of the module, this method could be
|
|
||||||
# called before the 'printing.printer.polling' table exists
|
|
||||||
# (but the model already is in memory)
|
|
||||||
return
|
|
||||||
if polling_obj.need_update():
|
|
||||||
self.start_printer_update()
|
|
||||||
return True
|
|
||||||
|
|
||||||
@api.v7
|
|
||||||
def browse(self, cr, uid, arg=None, context=None):
|
|
||||||
_super = super(PrintingPrinter, self)
|
|
||||||
recs = _super.browse(cr, uid, arg=arg, context=context)
|
|
||||||
if not recs._context.get('skip_update'):
|
|
||||||
recs.with_context(skip_update=True).update()
|
|
||||||
return recs
|
|
||||||
|
|
||||||
@api.v8
|
|
||||||
def browse(self, arg=None):
|
|
||||||
recs = super(PrintingPrinter, self).browse(arg=arg)
|
|
||||||
if not recs._context.get('skip_update'):
|
|
||||||
recs.with_context(skip_update=True).update()
|
|
||||||
return recs
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def set_default(self):
|
def set_default(self):
|
||||||
if not self:
|
if not self:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<openerp>
|
<openerp>
|
||||||
<data>
|
<data noupdate="1">
|
||||||
<!-- printing.action -->
|
<!-- printing.action -->
|
||||||
<record model="printing.action" id="printing_action_1">
|
<record model="printing.action" id="printing_action_1">
|
||||||
<field name="name">Send to Printer</field>
|
<field name="name">Send to Printer</field>
|
||||||
@@ -16,5 +16,19 @@
|
|||||||
<field name="fields_id" search="[('model','=','ir.actions.report.xml'),('name','=','property_printing_action')]"/>
|
<field name="fields_id" search="[('model','=','ir.actions.report.xml'),('name','=','property_printing_action')]"/>
|
||||||
<field name="value" eval="'printing.action,'+str(printing_action_2)"/>
|
<field name="value" eval="'printing.action,'+str(printing_action_2)"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record forcecreate="True" id="ir_cron_update_printers" model="ir.cron">
|
||||||
|
<field name="name">Update Printers Status</field>
|
||||||
|
<field eval="True" name="active"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall"/>
|
||||||
|
<field eval="'printing.printer'" name="model"/>
|
||||||
|
<field eval="'update_printers_status'" name="function"/>
|
||||||
|
<field eval="'()'" name="args"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|||||||
Reference in New Issue
Block a user