Skip to content

Commit

Permalink
Use a cron instead of threads to update printers status
Browse files Browse the repository at this point in the history
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 OCA#14
  • Loading branch information
guewen authored and gdgellatly committed Jan 30, 2019
1 parent 9fa62a2 commit e0e6893
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 217 deletions.
235 changes: 19 additions & 216 deletions base_report_to_printer/printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -299,40 +136,6 @@ def print_document(self, report, content, format):
_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:
Expand Down
16 changes: 15 additions & 1 deletion base_report_to_printer/printing_data.xml
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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>

0 comments on commit e0e6893

Please sign in to comment.