Merge branch 'master' into dejal

This commit is contained in:
David Sinclair 2022-09-21 18:41:02 -06:00
commit c12070124f
26 changed files with 837 additions and 256 deletions

View file

@ -133,10 +133,16 @@ You got the downtime message either through email or SMS. This is the order of o
When the new redis server is connected to the primary redis server:
# db-redis-story2 = moving to new server
# db-redis-story = old server about to be shutdown
# db-redis-story1 = old server about to be shutdown
# Edit digitalocean.tf to change db-redis-story count to 2
make plan
make apply
make firewall
# Wait for redis to sync, takes 5-10 minutes
# Edit redis/consul_service.json to switch primary to db-redis-story2
make celery_stop
make maintenance_on
apd -l db-redis-story2 -t replicaofnoone
aps -l db-redis-story,db-redis-story2 -t consul
aps -l db-redis-story1,db-redis-story2 -t consul
make maintenance_off
make task

View file

@ -21,6 +21,13 @@
become: yes
sysctl: name=vm.overcommit_memory value=1 state=present reload=yes
- name: Template redis.conf file
copy:
src: /srv/newsblur/docker/redis/redis.conf
dest: /srv/newsblur/docker/redis/redis.conf
notify: restart redis
register: updated_config
- name: Template redis_replica.conf file
template:
src: /srv/newsblur/docker/redis/redis_replica.conf.j2
@ -40,7 +47,7 @@
become: yes
docker_container:
name: redis
image: redis:6.2.7
image: redis:7
state: started
command: /usr/local/etc/redis/redis_server.conf
container_default_behavior: no_defaults

View file

@ -1,6 +1,6 @@
{
"service": {
{% if inventory_hostname in ["db-redis-user", "db-redis-story1", "db-redis-session", "db-redis-pubsub"] %}
{% if inventory_hostname in ["db-redis-user", "db-redis-story2", "db-redis-session", "db-redis-pubsub"] %}
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
{% else %}
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}-staging",

View file

@ -1404,7 +1404,12 @@ def load_river_stories__redis(request):
user_search = None
offset = (page-1) * limit
story_date_order = "%sstory_date" % ('' if order == 'oldest' else '-')
if user.pk == 86178:
# Disable Michael_Novakhov account
logging.user(request, "~FCLoading ~FMMichael_Novakhov~SN's river, resource usage too high, ignoring.")
return HttpResponse("Resource usage too high", status=429)
if infrequent:
feed_ids = Feed.low_volume_feeds(feed_ids, stories_per_month=infrequent)

View file

@ -52,6 +52,7 @@ def TaskFeeds():
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
logging.debug(" ---> ~FBFeeds being tasked: ~SB%s" % feeds)
@app.task(name='task-broken-feeds')
def TaskBrokenFeeds():

View file

@ -8,6 +8,7 @@ from django.views.decorators.http import condition
from django.http import HttpResponseForbidden, HttpResponseRedirect, HttpResponse, Http404
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
# from django.db import IntegrityError
from apps.rss_feeds.models import Feed, merge_feeds
from apps.rss_feeds.models import MFetchHistory
@ -510,19 +511,21 @@ def status(request):
return HttpResponseForbidden()
minutes = int(request.GET.get('minutes', 1))
now = datetime.datetime.now()
hour_ago = now + datetime.timedelta(minutes=minutes)
username = request.GET.get('user', '') or request.GET.get('username', '')
if username:
user = User.objects.get(username=username)
if username == "all":
feeds = Feed.objects.filter(next_scheduled_update__lte=hour_ago).order_by('next_scheduled_update')
else:
user = request.user
usersubs = UserSubscription.objects.filter(user=user)
feed_ids = usersubs.values('feed_id')
if minutes > 0:
hour_ago = now + datetime.timedelta(minutes=minutes)
feeds = Feed.objects.filter(pk__in=feed_ids, next_scheduled_update__lte=hour_ago).order_by('next_scheduled_update')
else:
hour_ago = now + datetime.timedelta(minutes=minutes)
feeds = Feed.objects.filter(pk__in=feed_ids, last_update__gte=hour_ago).order_by('-last_update')
if username:
user = User.objects.get(username=username)
else:
user = request.user
usersubs = UserSubscription.objects.filter(user=user)
feed_ids = usersubs.values('feed_id')
if minutes > 0:
feeds = Feed.objects.filter(pk__in=feed_ids, next_scheduled_update__lte=hour_ago).order_by('next_scheduled_update')
else:
feeds = Feed.objects.filter(pk__in=feed_ids, last_update__gte=hour_ago).order_by('-last_update')
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
queues = {

View file

@ -5,4 +5,5 @@ urlpatterns = [
url(r'^dashboard_graphs', views.dashboard_graphs, name='statistics-graphs'),
url(r'^feedback_table', views.feedback_table, name='feedback-table'),
url(r'^revenue', views.revenue, name='revenue'),
url(r'^slow', views.slow, name='slow'),
]

View file

@ -1,12 +1,22 @@
import base64
import pickle
import redis
import datetime
from operator import countOf
from collections import defaultdict
from django.http import HttpResponse
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import User
from django.conf import settings
from django.utils import feedgenerator
from django.http import HttpResponseForbidden
from apps.statistics.models import MStatistics, MFeedback
from apps.statistics.rstats import round_time
from apps.profile.models import PaymentHistory
from utils import log as logging
def dashboard_graphs(request):
statistics = MStatistics.all()
return render(
@ -49,4 +59,60 @@ def revenue(request):
request.META.get('HTTP_USER_AGENT', "")[:24]
))
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
@login_required
def slow(request):
r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL)
if not request.user.is_staff and not settings.DEBUG:
logging.user(request, "~SKNON-STAFF VIEWING SLOW STATUS!")
assert False
return HttpResponseForbidden()
now = datetime.datetime.now()
all_queries = {}
user_id_counts = {}
path_counts = {}
users = {}
for minutes_ago in range(60*6):
dt_ago = now - datetime.timedelta(minutes=minutes_ago)
minute = round_time(dt_ago, round_to=60)
dt_ago_str = minute.strftime("%a %b %-d, %Y %H:%M")
name = f"SLOW:{minute.strftime('%s')}"
minute_queries = r.lrange(name, 0, -1)
for query_raw in minute_queries:
query = pickle.loads(base64.b64decode(query_raw))
user_id = query['user_id']
if dt_ago_str not in all_queries:
all_queries[dt_ago_str] = []
if user_id in users:
user = users[user_id]
elif int(user_id) != 0:
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
continue
users[user_id] = user
else:
user = AnonymousUser()
users[user_id] = user
query['user'] = user
query['datetime'] = minute
all_queries[dt_ago_str].append(query)
if user_id not in user_id_counts:
user_id_counts[user_id] = 0
user_id_counts[user_id] += 1
if query['path'] not in path_counts:
path_counts[query['path']] = 0
path_counts[query['path']] += 1
user_counts = []
for user_id, count in user_id_counts.items():
user_counts.append({'user': users[user_id], 'count': count})
return render(request, 'statistics/slow.xhtml', {
'all_queries': all_queries,
'user_counts': user_counts,
'path_counts': path_counts,
})

View file

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.6.21'
repositories {
mavenCentral()
maven {
@ -9,9 +9,9 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.1'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
}
}
@ -30,22 +30,22 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.0'
implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.android.billingclient:billing:4.0.0'
implementation 'nl.dionsegijn:konfetti:1.2.2'
implementation 'com.google.android.play:core:1.10.3'
implementation "com.google.android.material:material:1.6.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.browser:browser:1.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.0"
implementation 'androidx.lifecycle:lifecycle-process:2.5.0'
implementation 'androidx.core:core-splashscreen:1.0.0-rc01'
implementation "com.google.dagger:hilt-android:2.40.1"
kapt "com.google.dagger:hilt-compiler:2.40.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-compiler:2.40.5"
}
android {
@ -54,8 +54,8 @@ android {
applicationId "com.newsblur"
minSdkVersion 21
targetSdkVersion 31
versionCode 204
versionName "12.0"
versionCode 205
versionName "12.0.1"
}
compileOptions.with {
sourceCompatibility = JavaVersion.VERSION_1_8

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:width="24dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="150">
<path

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:width="24dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="150">
<path

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#95968E"
android:pathData="M39.8,41.95 L26.65,28.8q-1.5,1.3 -3.5,2.025 -2,0.725 -4.25,0.725 -5.4,0 -9.15,-3.75T6,18.75q0,-5.3 3.75,-9.05 3.75,-3.75 9.1,-3.75 5.3,0 9.025,3.75 3.725,3.75 3.725,9.05 0,2.15 -0.7,4.15 -0.7,2 -2.1,3.75L42,39.75ZM18.85,28.55q4.05,0 6.9,-2.875Q28.6,22.8 28.6,18.75t-2.85,-6.925Q22.9,8.95 18.85,8.95q-4.1,0 -6.975,2.875T9,18.75q0,4.05 2.875,6.925t6.975,2.875Z" />
</vector>

View file

@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/rounded_corner_radius_4dp" />
</shape>

View file

@ -21,15 +21,14 @@
android:layout_height="match_parent"
android:layout_toRightOf="@id/story_item_favicon_borderbar_1" />
<com.google.android.material.imageview.ShapeableImageView
<ImageView
android:id="@+id/story_item_feedicon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_alignParentLeft="true"
android:layout_marginStart="18dp"
android:layout_marginTop="2dp"
android:layout_toRightOf="@+id/story_item_favicon_borderbar_2"
app:shapeAppearanceOverlay="@style/smallRoundImageShapeAppearance"/>
android:layout_toRightOf="@+id/story_item_favicon_borderbar_2" />
<TextView
android:id="@+id/story_item_feedtitle"

View file

@ -1,7 +1,7 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Keep in sync with ShortcutUtils-->
<shortcut
android:icon="@drawable/ic_search"
android:icon="@drawable/ic_search_2"
android:shortcutId="all_stories_search"
android:shortcutShortLabel="@string/search">
<intent

View file

@ -16,6 +16,7 @@ import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_METADATA
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
import com.newsblur.util.UIUtils.syncUpdateStatus
import java.lang.IllegalStateException
import java.util.*
class FeedUtils(
@ -487,8 +488,13 @@ class FeedUtils(
// NB: when our minSDKversion hits 28, it could be possible to start the service via the JobScheduler
// with the setImportantWhileForeground() flag via an enqueue() and get rid of all legacy startService
// code paths
val i = Intent(context, NBSyncService::class.java)
context.startService(i)
try {
val i = Intent(context, NBSyncService::class.java)
context.startService(i)
} catch (e: IllegalStateException) {
// BackgroundServiceStartNotAllowedException
Log.e(this, "triggerSync error: ${e.message}")
}
}
/**

View file

@ -7,8 +7,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.newsblur.R;
import com.newsblur.activity.Profile;
import com.newsblur.view.FlowLayout;
@ -17,21 +16,21 @@ public class ViewUtils {
private ViewUtils() {} // util class - no instances
public static ImageView createSharebarImage(final Context context, final String photoUrl, final String userId, ImageLoader iconLoader) {
ShapeableImageView image = new ShapeableImageView(context);
ImageView image = new ImageView(context);
int imageLength = UIUtils.dp2px(context, 15);
image.setMaxHeight(imageLength);
image.setMaxWidth(imageLength);
ShapeAppearanceModel shape = new ShapeAppearanceModel().withCornerSize(UIUtils.dp2px(context, 4));
image.setShapeAppearanceModel(shape);
image.setClipToOutline(true);
image.setBackgroundResource(R.drawable.shape_rounded_corners_4dp);
FlowLayout.LayoutParams imageParameters = new FlowLayout.LayoutParams(5, 5);
imageParameters.height = imageLength;
imageParameters.width = imageLength;
image.setMaxHeight(imageLength);
image.setMaxWidth(imageLength);
image.setLayoutParams(imageParameters);
iconLoader.displayImage(photoUrl, image);
image.setOnClickListener(new OnClickListener() {

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,6 @@
.NB-status td {
border-top: 1px solid #F0F0F0;
margin: 0;
padding: 0 0;
padding: 0 6px 0 0;
max-width: 300px;
}

View file

@ -85,7 +85,7 @@
<img src="/media/img/logo_512.png" class="logo">
<h1>NewsBlur is in <span class="error404">maintenance mode</span></h1>
<div class="description">
<p>Moving to a larger Redis story DB, since the existing DB is buckling under the new load from the requirements of the new NewsBlur Premium Archive subscription.</p>
<p>Moving to another Redis story DB, since the existing DB is having some issues. Check the daily load time graph to see how it's had an impact. Always good to do this, expect fast load times after this.</p>
<p>To pass the time, <a href="http://mltshp.com/popular">check out what's popular on MLTSHP</a>.</p>
</div>
</div>

View file

@ -2,7 +2,7 @@
{% load utils_tags tz %}
{% block bodyclass %}NB-body-status{% endblock %}
{% block bodyclass %}NB-body-status NB-static{% endblock %}
{% block content %}
@ -21,6 +21,7 @@
<th style="white-space: nowrap">Last Update<br>Next Update</th>
<th>Min to<br>next update</th>
<th>Decay</th>
<th>Last fetch</th>
<th>Subs</th>
<th>Active</th>
<th>Premium</th>
@ -29,11 +30,13 @@
<th>Act. Prem</th>
<th>Per Month</th>
<th>Last Month</th>
<th>In Archive</th>
<th>File size (b)</th>
</tr>
{% for feed in feeds %}
<tr>
<td>{{ feed.pk }}</td>
<td><img class="NB-favicon" src="/rss_feeds/icon/{{ feed.pk }}" /> {{ feed.feed_title|truncatewords:4 }}</td>
<td title="{{ feed.feed_address }}"><img class="NB-favicon" src="/rss_feeds/icon/{{ feed.pk }}" /> {{ feed.feed_title|truncatewords:4 }}</td>
<td>{{ feed.last_update|smooth_timedelta }}</td>
<td class="NB-status-update" style="white-space: nowrap">
{% localdatetime feed.last_update "%b %d, %Y %H:%M:%S" %}
@ -42,6 +45,7 @@
</td>
<td>{{ feed.next_scheduled_update|smooth_timedelta }}</td>
<td>{{ feed.min_to_decay }}</td>
<td>{{ feed.last_load_time }}</td>
<td>{{ feed.num_subscribers }}</td>
<td style="color: {% if feed.active_subscribers == 0 %}lightgrey{% else %}darkblue{% endif %}">{{ feed.active_subscribers }}</td>
<td style="color: {% if feed.premium_subscribers == 0 %}lightgrey{% else %}darkblue{% endif %}">{{ feed.premium_subscribers }}</td>
@ -50,6 +54,8 @@
<td style="color: {% if feed.active_premium_subscribers == 0 %}lightgrey{% else %}darkblue{% endif %}">{{ feed.active_premium_subscribers }}</td>
<td style="color: {% if feed.average_stories_per_month == 0 %}lightgrey{% else %}{% endif %}">{{ feed.average_stories_per_month }}</td>
<td style="color: {% if feed.stories_last_month == 0 %}lightgrey{% else %}{% endif %}">{{ feed.stories_last_month }}</td>
<td style="color: {% if feed.archive_count == 0 %}lightgrey{% else %}{% endif %}">{{ feed.archive_count }}</td>
<td style="color: {% if feed.fs_size_bytes == 0 %}lightgrey{% else %}{% endif %}">{{ feed.fs_size_bytes|commify }}</td>
</tr>
{% endfor %}

View file

@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% load utils_tags tz %}
{% block bodyclass %}NB-body-status NB-static{% endblock %}
{% block content %}
<div class="NB-module">
<div class="queries">
<table class="NB-status">
{% for user_count in user_counts %}
<tr>
{% if forloop.first %}<td rowspan={{user_counts|length}} valign=top><b>Users</b>{% endif %}
<td><b>{{ user_count.user }}</b></td>
<td>{{ user_count.count }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="queries">
<table class="NB-status">
{% for path, count in path_counts.items %}
<tr>
{% if forloop.first %}<td rowspan={{path_counts|length}} valign=top><b>Paths</b>{% endif %}
<td><b>{{ path }}</b></td>
<td>{{ count }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<table class="NB-status">
{% for dt_str, queries in all_queries.items %}
{% for query in queries %}
<tr>
{% if forloop.first %}
<td rowspan={{ queries|length }} valign=top> <b>
{% localdatetime query.datetime "%a %b %d, %Y %H:%M" %}
</b></td>
{% endif %}
<td>{{ query.user }}</td>
<td>{{ query.time }}</td>
<td>{{ query.method }}</td>
<td>{{ query.path }}</td>
<td>{% if query.data %}{{ query.data }}{% endif %}</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock content %}

View file

@ -380,11 +380,12 @@ resource "digitalocean_droplet" "db-redis-sessions" {
}
resource "digitalocean_droplet" "db-redis-story" {
count = 1
count = 2
image = var.droplet_os
name = "db-redis-story${count.index+1}"
region = var.droplet_region
size = contains([1], count.index) ? "m-8vcpu-64gb" : var.redis_story_droplet_size
# size = var.redis_story_droplet_size
ssh_keys = [digitalocean_ssh_key.default.fingerprint]
provisioner "local-exec" {
command = "/srv/newsblur/ansible/utils/generate_inventory.py; sleep 120"
@ -466,12 +467,13 @@ resource "digitalocean_droplet" "db-postgres" {
# servers=$(for i in {1..9}; do echo -n "-target=\"digitalocean_droplet.db-mongo-primary[$i]\" " ; done); tf plan -refresh=false `eval echo $servers`
#
resource "digitalocean_droplet" "db-mongo-primary" {
count = 1
count = 2
backups = contains([0], count.index) ? false : true
image = var.droplet_os
name = "db-mongo-primary${count.index+1}"
region = var.droplet_region
size = contains([1], count.index) ? "m3-8vcpu-64gb" : var.mongo_primary_droplet_size
# size = contains([1], count.index) ? "m3-8vcpu-64gb" : var.mongo_primary_droplet_size
size = var.mongo_primary_droplet_size
ssh_keys = [digitalocean_ssh_key.default.fingerprint]
provisioner "local-exec" {
command = "/srv/newsblur/ansible/utils/generate_inventory.py; sleep 120"

View file

@ -89,5 +89,5 @@ variable "elasticsearch_droplet_size" {
variable "redis_story_droplet_size" {
type = string
default = "m-8vcpu-64gb"
default = "m-4vcpu-32gb"
}

View file

@ -1,11 +1,17 @@
from django.conf import settings
from utils import log as logging
from apps.statistics.rstats import round_time
import pickle
import base64
import time
import redis
IGNORE_PATHS = [
"/_haproxychk",
]
RECORD_SLOW_REQUESTS_ABOVE_SECONDS = 10
class DumpRequestMiddleware:
def process_request(self, request):
if settings.DEBUG and request.path not in IGNORE_PATHS:
@ -40,22 +46,31 @@ class DumpRequestMiddleware:
redis_log
))
return response
def elapsed_time(self, request):
time_elapsed = ""
if hasattr(request, 'start_time'):
seconds = time.time() - request.start_time
color = '~FB'
if seconds >= 1:
color = '~FR'
elif seconds > .2:
color = '~SB~FK'
time_elapsed = "[%s%.4ss~SB] " % (
color,
seconds,
)
return time_elapsed
if seconds > RECORD_SLOW_REQUESTS_ABOVE_SECONDS:
r = redis.Redis(connection_pool=settings.REDIS_STATISTICS_POOL)
pipe = r.pipeline()
minute = round_time(round_to=60)
name = f"SLOW:{minute.strftime('%s')}"
user_id = request.user.pk if request.user.is_authenticated else "0"
data_string = None
if request.method == "GET":
data_string = ' '.join([f"{key}={value}" for key, value in request.GET.items()])
elif request.method == "GET":
data_string = ' '.join([f"{key}={value}" for key, value in request.POST.items()])
data = {
"user_id": user_id,
"time": round(seconds, 2),
"path": request.path,
"method": request.method,
"data": data_string,
}
pipe.lpush(name, base64.b64encode(pickle.dumps(data)).decode('utf-8'))
pipe.expire(name, 60*60*12) # 12 hours
pipe.execute()
return response
def color_db(self, seconds, default):
color = default