Как djinni переезжал с Python2 на Python3
Статья с набором действий для тех, кто тоже переезжает. 2to3 делает основную рутину, но еще штук 40 кейсов я погуглила за вас :) Может сэкономлю кому-то 40×20мин.
Полтора года назад задепрекейтили Python2, но особо не мешало, поэтому продолжали сидеть на старом до последнего. В последние месяцы уже начали появляться либы у платежных систем с нужными фичами, которые уже без поддержки Python2.
Исходная инфа: проект без тестов, на Python2.7 и django1.11. Еще jinja2, redis, rq, PostgreSQL, elasticsearch. В целом код простой, питон-части около 50 тыс. строк, еще столько же темплейтов.
Обновлять решила до Python3.7 и django2.2. Почему 3.7, а не 3.9 — у какой-то из старых либ был этот потолок, хотя её потом тоже заменила. Почему django2, а не 3 — это основная либа, для снижения рисков переход сделала чуть меньше.
На момент написания статьи уже 2 недели выждали, метрики и мониторинги проверили, 99% багов точно выловили :)
Переезд получился из таких стадий:
- 1 выходной «копнуть» попробовать, обновлений того, во что уперлась;
- следующий месяц заменила в спокойном режиме 3 либы;
- и за еще один выходной произошел переезд.
График нагрузки прода CPU% остался таким же, выигрыша в производительности не случилось.
Тестовый переезд
Либы были описаны в requirements.txt и запиненные версии и dependencies в constraints.txt (просто перепаст pip freeze
)
Начала с того, что зачистила оба — и requirements.txt, и constraints.txt. Потом взяла контейнер с 3.7 питоном и сразу дописывала то, что точно используется. В конце потом примерно треть либ не вернулась — старые неиспользуемые.
Несовместимости сразу вылезли только на 3 либах, и в течение следующего месяца их попереводила на аналоги, в спокойном режиме, где-то между основными задачами.
- django_elasticsearch_dsl — избавилась от elasticsearch в пользу Postgres Fulltext Search, чтобы работать в рамках одной базы. Объёмы поиска небольшие, поэтому в скорости особо не потеряли. Тут еще были небольшие приключения с конвертацией boolean search из формата elasticsearch в postgres fulltext search.
- mandrill перевела на свежую mailchimp-transactional (он только для python3, поэтому в код вмержила уже в момент переезда)
- oauth2client перешла на свежую google-auth
Все остальное вроде подхватилось и работало, после небольших фиксов.
2to3
После замены либ, на выходных уже сделала полноценный переезд.
Первым действием был 2to3, делала 3 отдельных комита:
- отдельно перевод print-ов (на со скобками)
- отдельно unicode (без u’’ перед строкой)
- и отдельно все остальное, чтобы глазами проверить:
2to3-2.7 -w -f print *.py 2to3-2.7 -w -f unicode *.py 2to3-2.7 -w -x print -x unicode *.py rm *.bak
Еще изменения в Python
- В местах, связанных с чтением или записью файлов — удалила
.decode('utf8')
- Аналогично при открытии файлов:
with open(fname, 'r') as fd:
изменилось на
with open(fname, 'rb') as fd:
- Но в местах работы с hashlib наоборот некоторым параметрам добавила
.decode('utf8')
чтобы привести к bytes, либо.encode('utf8')
, чтобы привести к строке результат хеширования, он там чётко ошибки пишет кого и где не хватает - в местах использования urllib поубирала
.encode('utf8')
, urllib теперь сам разбирается что и когда ему нужно - При сравнении с integer, None перестал превращаться в 0, в местах проверок:
if self.salary_min >= self.salary75:
изменилось на
if self.salary_min and self.salary75 and self.salary_min >= self.salary75:
- аналогичное изменение в jinja2-темплейтах, тоже добавила проверки на не-None перед тем как сравнивать int
__unicode__
позаменяла на__str__
- один из ключей к внешним сервисам теперь тоже потребовал быть в bytes:
MY_SOME_KEY = 'aksdfj83jlaksdjf'
изменилось на
MY_SOME_KEY = b'aksdfj83jlaksdjf'
- там, где нужен был англ. алфавит:
string.letters string.uppercase
изменилось на
string.ascii_letters string.ascii_uppercase
- в одной из функций получилось, что на вход могли приходить и bytes, и str, работать надо было дальше с str, добавила конвертацию просто по ситуации:
if type(v) == bytes: v = v.decode('utf8')
- в некоторых местах для дебага у меня была такая конструкция, в python3 это уже не актуально, её просто поудаляла:
import sys reload(sys) sys.setdefaultencoding('utf8')
- для передачи в темплейт выборки из джанги уже не прокатывал просто q:
recruiters = q[:25].all()
изменилось на
recruiters = list(q[:25].all())
Изменения в django для переезда 1.11 → 2.2.20
- в settings.py
MESSAGE_LEVEL = 'ERROR'
поменялся на
MESSAGE_LEVEL = logging.ERROR
- еще в settings.py в
TEMPLATES
нужно было добавить для django admin:
TEMPLATES = [ ... "OPTIONS": { "context_processors": [ "django.contrib.messages.context_processors.messages", ... ], } ...
- запуск всех скриптов переписала запускаться как
python3 manage.py ...
- is_authenticated:
user.is_authenticated()
поменялся на
user.is_authenticated
- у класса модели админки, если перебивались fields, то теперь это нужно было делать функцией:
class CandidateAdmin(admin.ModelAdmin): ... fields = ['email', ...
изменилось на
class CandidateAdmin(admin.ModelAdmin): @property def fields(self): fields = ...
- у моделей
on_delete
стал обязательным параметром, там где его не было, добавила тот, который раньше был дефолтным:
company = models.ForeignKey(Company, on_delete=models.CASCADE, ...
- у формочек первый параметр с выбором теперь должен стать именованным:
country = forms.ChoiceField(Recruiter.COUNTRIES, required=True)
изменилось на
country = forms.ChoiceField(choices=Recruiter.COUNTRIES, required=True)
- в django admin, там где генерились спец.поля с html-кодом, изменился формат:
class CandidateAdmin(admin.ModelAdmin): ... def emaillead_list(self, obj): res = '<a href="/my/link">=subs=</a>'.format(obj.email) return res emaillead_list.allow_tags = True
изменилось на
class CandidateAdmin(admin.ModelAdmin): ... def emaillead_list(self, obj): res = '<a href="/my/link">=subs=</a>'.format(obj.email) return mark_safe(res)
- для авторизации (у нас сессии на куках), новые параметры в settings.py:
SESSION_COOKIE_SAMESITE = None SESSION_COOKIE_SECURE = True
Изменения из-за обновления либ
- свежий redis по умолчанию стал работать с bytes, но можно вернуть на str:
redis_main = redis.Redis(settings.REDIS_HOST)
изменилось на
redis_main = redis.Redis(settings.REDIS_HOST, decode_responses=True)
- свежий redis перестал конвертить boolean в string, поэтому в местах хранения boolean:
redis_main.set(redis_key, is_hot)
изменилось на
redis_main.set(redis_key, int(is_hot))
- uwsgi тоже изменилась либа, в Dockerfile:
RUN apt-get install -y ... uwsgi-plugin-python python3-pip ...
изменилось на
RUN apt-get install -y ... uwsgi-plugin-python3 python3-pip ...
- и при запуске uwsgi:
uwsgi ... --plugin=python3 ... --callable application ...
изменилось на
uwsgi ... --plugin=python3 ... --module=djinn.wsgi:application ...
- при отправке attachments через mailchimp-transactional, при формировании json:
x.append(dict( type='image/png', name='graph.png', content=fdata.encode('base64') ))
заменился на
x.append(dict( type='image/png', name='graph.png', content=base64.b64encode(fdata).decode('utf8') ))
- для отправки писем через mailchimp-transactional (был mandrill) уже не стало нужно кодировать subj:
subject = (_('Your attachment was not delivered')).encode('utf8')
изменилось на
subject = (_('Your attachment was not delivered'))
- у свежего rq исчез класс
FailedQueue
, теперь fail очередь достаётся так:
fq = Queue('failed', connection=Redis(settings.REDIS_HOST, db=settings.REDIS_RQ_NUM))
- у свежего rq забирать задачу теперь так:
for q_id in myqueue.get_job_ids()[-10:]: j = fq.fetch_job(q_id)
изменилось на
for q_id in myqueue.get_job_ids()[-10:]: j = Job.fetch(q_id, connection=RQ_CONN)
- результат выполнения subprocess теперь приходит в bytes, добавила
.decode('utf8')
для работы с ним как со строкой - еще в одном месте при работе с файлами и xlsxwriter.workbook использовалось StringIO, заменила на BytesIO:
output = StringIO.StringIO() ... row[0] = something.encode('utf-8')
изменилось на
output = BytesIO() ... row[0] = something
- в failed очереди rq остались еще пару десятков не сильно важных задач (сохранение логов-поисковых запросов), к которым нет доступа в новом rq, удаляла их так:
from djinn import settings from redis import Redis from rq.queue import Queue qf = Queue('failed', connection=Redis(settings.REDIS_HOST, db=settings.REDIS_RQ_NUM)) len(qf) qf.empty()
Чеклист проверки на проде, у нас же нет тестов ;)
- recruiter
- candidate
- anonymous
- pdf invoices
- email (forgot password)
- /pulse maillist
- /analytics (cand, recr, anonymous)
- /salaries (cand, recr, anonymous)
- test rq (searchlog)
- djinni_jobs_bot (привязка, отвязка, создание широкой подписки с переспросом, удаление подписки)
- djinni_search_bot (привязка, отвязка, создание широкой подписки с переспросом, удаление подписки)
- подписки на вакансии, которые приходят в бота
- !!! соц.авторизация
- test slackdeploy
- !!! подписки на кандидатов, которые приходят в бота
- расссылки вторник утром
- /admin/health
- export xls ious and hirelists (custom_billing)
- import jobs
- оплаты braintree
- оплаты ukr
- оплаты wayforpay
- rq scripts fail очередь протестить
В целом переезд прошел гладко — основной downtime был пару минут в выходной и несколько багов точечно в разных кейсах в последующие дни. Выше перечислены все фиксы, которые понадобились.
65 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів