cache last used service record in DB

This commit is contained in:
Daniel Gultsch 2023-03-03 10:14:02 +01:00
parent 807078b24f
commit 2e5e2ff6fe
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
8 changed files with 231 additions and 17 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "bc04f3d0c58f7e50f5c7973a7a06c9eb", "identityHash": "b5e8a59bbd86e133c0bc2edd303ad2a0",
"entities": [ "entities": [
{ {
"tableName": "account", "tableName": "account",
@ -2432,12 +2432,103 @@
] ]
} }
] ]
},
{
"tableName": "service_record_cache",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `domain` TEXT NOT NULL, `ip` BLOB, `hostname` TEXT, `port` INTEGER NOT NULL, `directTls` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `authenticated` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serviceRecord.ip",
"columnName": "ip",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "serviceRecord.hostname",
"columnName": "hostname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "serviceRecord.port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceRecord.directTls",
"columnName": "directTls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceRecord.priority",
"columnName": "priority",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceRecord.authenticated",
"columnName": "authenticated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_service_record_cache_accountId_domain",
"unique": true,
"columnNames": [
"accountId",
"domain"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_record_cache_accountId_domain` ON `${TABLE_NAME}` (`accountId`, `domain`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
} }
], ],
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc04f3d0c58f7e50f5c7973a7a06c9eb')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b5e8a59bbd86e133c0bc2edd303ad2a0')"
] ]
} }
} }

View file

@ -17,6 +17,7 @@ import im.conversations.android.database.dao.MessageDao;
import im.conversations.android.database.dao.NickDao; import im.conversations.android.database.dao.NickDao;
import im.conversations.android.database.dao.PresenceDao; import im.conversations.android.database.dao.PresenceDao;
import im.conversations.android.database.dao.RosterDao; import im.conversations.android.database.dao.RosterDao;
import im.conversations.android.database.dao.ServiceRecordDao;
import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.AccountEntity;
import im.conversations.android.database.entity.AvatarAdditionalEntity; import im.conversations.android.database.entity.AvatarAdditionalEntity;
import im.conversations.android.database.entity.AvatarEntity; import im.conversations.android.database.entity.AvatarEntity;
@ -49,6 +50,7 @@ import im.conversations.android.database.entity.NickEntity;
import im.conversations.android.database.entity.PresenceEntity; import im.conversations.android.database.entity.PresenceEntity;
import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.entity.RosterItemEntity;
import im.conversations.android.database.entity.RosterItemGroupEntity; import im.conversations.android.database.entity.RosterItemGroupEntity;
import im.conversations.android.database.entity.ServiceRecordCacheEntity;
@Database( @Database(
entities = { entities = {
@ -83,7 +85,8 @@ import im.conversations.android.database.entity.RosterItemGroupEntity;
PresenceEntity.class, PresenceEntity.class,
MessageReactionEntity.class, MessageReactionEntity.class,
RosterItemEntity.class, RosterItemEntity.class,
RosterItemGroupEntity.class RosterItemGroupEntity.class,
ServiceRecordCacheEntity.class
}, },
version = 1) version = 1)
@TypeConverters(Converters.class) @TypeConverters(Converters.class)
@ -130,4 +133,6 @@ public abstract class ConversationsDatabase extends RoomDatabase {
public abstract PresenceDao presenceDao(); public abstract PresenceDao presenceDao();
public abstract RosterDao rosterDao(); public abstract RosterDao rosterDao();
public abstract ServiceRecordDao serviceRecordDao();
} }

View file

@ -2,7 +2,10 @@ package im.conversations.android.database;
import androidx.room.TypeConverter; import androidx.room.TypeConverter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import de.measite.minidns.DNSName;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Instant; import java.time.Instant;
import org.jxmpp.jid.BareJid; import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
@ -145,4 +148,31 @@ public final class Converters {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@TypeConverter
public static String fromDNSName(final DNSName dnsName) {
return dnsName == null ? null : dnsName.toString();
}
@TypeConverter
public static DNSName toDNSName(final String dnsName) {
return dnsName == null ? null : DNSName.from(dnsName);
}
@TypeConverter
public static byte[] fromInetAddress(final InetAddress inetAddress) {
return inetAddress == null ? null : inetAddress.getAddress();
}
@TypeConverter
public static InetAddress toInetAddress(final byte[] address) {
if (address == null || address.length == 0) {
return null;
}
try {
return InetAddress.getByAddress(address);
} catch (final UnknownHostException e) {
return null;
}
}
} }

View file

@ -0,0 +1,32 @@
package im.conversations.android.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import im.conversations.android.database.entity.ServiceRecordCacheEntity;
import im.conversations.android.database.model.Account;
import im.conversations.android.dns.ServiceRecord;
@Dao
public abstract class ServiceRecordDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract void insert(ServiceRecordCacheEntity entity);
public void insert(final Account account, final ServiceRecord serviceRecord) {
insert(
ServiceRecordCacheEntity.of(
account, account.address.getDomain().toString(), serviceRecord));
}
@Query(
"SELECT ip,hostname,port,directTls,priority,authenticated FROM service_record_cache"
+ " WHERE accountId=:account AND domain=:domain LIMIT 1")
protected abstract ServiceRecord getCachedServiceRecord(
final long account, final String domain);
public ServiceRecord getCachedServiceRecord(final Account account) {
return getCachedServiceRecord(account.id, account.address.getDomain().toString());
}
}

View file

@ -0,0 +1,44 @@
package im.conversations.android.database.entity;
import androidx.annotation.NonNull;
import androidx.room.Embedded;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import im.conversations.android.database.model.Account;
import im.conversations.android.dns.ServiceRecord;
@Entity(
tableName = "service_record_cache",
foreignKeys =
@ForeignKey(
entity = AccountEntity.class,
parentColumns = {"id"},
childColumns = {"accountId"},
onDelete = ForeignKey.CASCADE),
indices = {
@Index(
value = {"accountId", "domain"},
unique = true)
})
public class ServiceRecordCacheEntity {
@PrimaryKey(autoGenerate = true)
public Long id;
@NonNull public Long accountId;
@NonNull public String domain;
@Embedded @NonNull public ServiceRecord serviceRecord;
public static ServiceRecordCacheEntity of(
final Account account, final String domain, final ServiceRecord serviceRecord) {
final var entity = new ServiceRecordCacheEntity();
entity.accountId = account.id;
entity.domain = domain;
entity.serviceRecord = serviceRecord;
return entity;
}
}

View file

@ -41,6 +41,7 @@ import org.jxmpp.jid.DomainJid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@SuppressWarnings("UnstableApiUsage")
public class Resolver { public class Resolver {
private static final Logger LOGGER = LoggerFactory.getLogger(Resolver.class); private static final Logger LOGGER = LoggerFactory.getLogger(Resolver.class);

View file

@ -59,6 +59,10 @@ public class ServiceRecord implements Comparable<ServiceRecord> {
return port; return port;
} }
public int getPriority() {
return this.priority;
}
public DNSName getHostname() { public DNSName getHostname() {
return hostname; return hostname;
} }

View file

@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -338,22 +339,30 @@ public class XmppConnection implements Runnable {
LOGGER.warn("Resolver results were empty"); LOGGER.warn("Resolver results were empty");
return; return;
} }
final List<ServiceRecord> resultsWithBackup;
final ServiceRecord storedBackupResult; final ServiceRecord storedBackupResult;
if (connection != null) { if (connection != null) {
storedBackupResult = null; storedBackupResult = null;
resultsWithBackup = results;
} else { } else {
// TODO fix resolver result caching
storedBackupResult = storedBackupResult =
null; // context.databaseBackend.findResolverResult(domain); ConversationsDatabase.getInstance(context)
.serviceRecordDao()
.getCachedServiceRecord(account);
if (storedBackupResult != null && !results.contains(storedBackupResult)) { if (storedBackupResult != null && !results.contains(storedBackupResult)) {
results.add(storedBackupResult); resultsWithBackup =
new ImmutableList.Builder<ServiceRecord>()
.addAll(results)
.add(storedBackupResult)
.build();
LOGGER.debug( LOGGER.debug(
account.address "loaded backup resolver result from db {}", storedBackupResult);
+ ": loaded backup resolver result from db: " } else {
+ storedBackupResult); resultsWithBackup = results;
} }
} }
for (Iterator<ServiceRecord> iterator = results.iterator(); iterator.hasNext(); ) { for (final Iterator<ServiceRecord> iterator = resultsWithBackup.iterator();
iterator.hasNext(); ) {
final ServiceRecord result = iterator.next(); final ServiceRecord result = iterator.next();
if (Thread.currentThread().isInterrupted()) { if (Thread.currentThread().isInterrupted()) {
LOGGER.debug(account.address + ": Thread was interrupted"); LOGGER.debug(account.address + ": Thread was interrupted");
@ -362,9 +371,8 @@ public class XmppConnection implements Runnable {
try { try {
// if tls is true, encryption is implied and must not be started // if tls is true, encryption is implied and must not be started
this.encryptionEnabled = result.isDirectTls(); this.encryptionEnabled = result.isDirectTls();
verifiedHostname = this.verifiedHostname =
result.isAuthenticated() ? result.getHostname().toString() : null; result.isAuthenticated() ? result.getHostname().toString() : null;
LOGGER.debug("verified hostname " + verifiedHostname);
final InetSocketAddress addr; final InetSocketAddress addr;
if (result.getIp() != null) { if (result.getIp() != null) {
addr = new InetSocketAddress(result.getIp(), result.getPort()); addr = new InetSocketAddress(result.getIp(), result.getPort());
@ -403,12 +411,11 @@ public class XmppConnection implements Runnable {
localSocket.setSoTimeout(ConnectionPool.SOCKET_TIMEOUT * 1000); localSocket.setSoTimeout(ConnectionPool.SOCKET_TIMEOUT * 1000);
if (startXmpp(localSocket)) { if (startXmpp(localSocket)) {
localSocket.setSoTimeout( localSocket.setSoTimeout(0);
0); // reset to 0; once the connection is established we dont
// want this
if (connection == null && !result.equals(storedBackupResult)) { if (connection == null && !result.equals(storedBackupResult)) {
// TODO store resolver result ConversationsDatabase.getInstance(context)
// context.databaseBackend.saveResolverResult(domain, result); .serviceRecordDao()
.insert(account, result);
} }
break; // successfully connected to server that speaks xmpp break; // successfully connected to server that speaks xmpp
} else { } else {