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 os
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from tempfile import mkstemp
|
||||
from threading import Thread
|
||||
|
||||
import cups
|
||||
import psycopg2
|
||||
|
||||
from openerp import models, fields, api, sql_db
|
||||
from openerp.tools import ormcache
|
||||
from openerp import models, fields, api
|
||||
|
||||
_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):
|
||||
"""
|
||||
@@ -240,6 +59,24 @@ class PrintingPrinter(models.Model):
|
||||
location = fields.Char(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
|
||||
def _prepare_update_from_cups(self, cups_connection, cups_printer):
|
||||
mapping = {
|
||||
@@ -299,40 +136,6 @@ class PrintingPrinter(models.Model):
|
||||
_logger.info("Printing job: '%s'" % file_name)
|
||||
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
|
||||
def set_default(self):
|
||||
if not self:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<data noupdate="1">
|
||||
<!-- printing.action -->
|
||||
<record model="printing.action" id="printing_action_1">
|
||||
<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="value" eval="'printing.action,'+str(printing_action_2)"/>
|
||||
</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>
|
||||
</openerp>
|
||||
|
||||
Reference in New Issue
Block a user