first version of transactor (todo-app working)
This commit is contained in:
parent
7dc3475e35
commit
df92d3a55d
4 changed files with 161 additions and 83 deletions
|
|
@ -21,9 +21,7 @@ type
|
||||||
# RequestMessage {.importc.} = ref object
|
# RequestMessage {.importc.} = ref object
|
||||||
|
|
||||||
let conn = newWebSocket("ws://localhost:8080", "karaxdb")
|
let conn = newWebSocket("ws://localhost:8080", "karaxdb")
|
||||||
var gid: MessageId
|
var version: int
|
||||||
var pendingIds = newJDict[MessageId, Message]()
|
|
||||||
var pending: int
|
|
||||||
|
|
||||||
#proc loadDb*(url: cstring): Db =
|
#proc loadDb*(url: cstring): Db =
|
||||||
# result = nil
|
# result = nil
|
||||||
|
|
@ -34,11 +32,9 @@ proc newTransaction*(): Db =
|
||||||
proc insert*(head, newdb: Db) =
|
proc insert*(head, newdb: Db) =
|
||||||
newdb.next = head
|
newdb.next = head
|
||||||
#result = newdb
|
#result = newdb
|
||||||
let expectedVersion = if not head.isNil: head.version + 1 else: 1
|
let expectedVersion = version
|
||||||
inc gid.int
|
inc version
|
||||||
let m = Message(kind: NewData, data: newdb.data, version: expectedVersion, id: gid)
|
let m = Message(kind: NewData, data: newdb.data, version: expectedVersion, id: MessageId(0))
|
||||||
pendingIds[gid] = m
|
|
||||||
inc pending
|
|
||||||
conn.send(toJson(m))
|
conn.send(toJson(m))
|
||||||
|
|
||||||
proc merge*(newer, older: Db) =
|
proc merge*(newer, older: Db) =
|
||||||
|
|
@ -49,28 +45,15 @@ proc registerOnUpdate*(update: proc(db: Db)) =
|
||||||
proc (e: MessageEvent) =
|
proc (e: MessageEvent) =
|
||||||
let msg = fromJson[Message](e.data)
|
let msg = fromJson[Message](e.data)
|
||||||
case msg.kind
|
case msg.kind
|
||||||
of Conflict:
|
of Rejected:
|
||||||
# conflict, so throw away the sent data, don't apply the changes:
|
# conflict, so throw away the sent data, don't apply the changes:
|
||||||
if pending > 0:
|
kout cstring"rejected"
|
||||||
pendingIds.del msg.id
|
|
||||||
dec pending
|
|
||||||
# server sent data that caused the conflict:
|
|
||||||
if msg.data.len > 0:
|
|
||||||
let db = Db(data: msg.data, version: msg.version)
|
|
||||||
update(db)
|
|
||||||
of Accepted:
|
|
||||||
kout cstring"accepted", pending
|
|
||||||
if pending > 0:
|
|
||||||
let d = pendingIds[msg.id]
|
|
||||||
# data was not submitted again, so we use the in-memory version
|
|
||||||
# of the data:
|
|
||||||
let db = Db(data: d.data, version: d.version)
|
|
||||||
pendingIds.del msg.id
|
|
||||||
dec pending
|
|
||||||
update(db)
|
|
||||||
of Newdata:
|
of Newdata:
|
||||||
let db = Db(data: msg.data, version: msg.version)
|
let db = Db(data: msg.data, version: msg.version)
|
||||||
update(db)
|
update(db)
|
||||||
|
of Disconnect:
|
||||||
|
kout cstring"disconnected"
|
||||||
|
else: kout cstring"something else"
|
||||||
|
|
||||||
iterator list*(db: Db, q: Query): Triple =
|
iterator list*(db: Db, q: Query): Triple =
|
||||||
# XXX here the datamodel comes in! We must not
|
# XXX here the datamodel comes in! We must not
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,4 @@ type
|
||||||
MessageId* = distinct int
|
MessageId* = distinct int
|
||||||
|
|
||||||
MessageKind* = enum
|
MessageKind* = enum
|
||||||
Newdata, Conflict, Accepted
|
Newdata, Rejected, Disconnect
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
|
|
||||||
import asynchttpserver, asyncdispatch, asyncnet, "../../../websocket/websocket", common, json
|
|
||||||
|
|
||||||
type
|
|
||||||
Message = object
|
|
||||||
kind: MessageKind
|
|
||||||
id: MessageId
|
|
||||||
data: seq[Triple]
|
|
||||||
version: int
|
|
||||||
|
|
||||||
proc `%`(id: MessageId): JsonNode = %BiggestInt(id)
|
|
||||||
proc `%`(k: MessageKind): JsonNode = %BiggestInt(k)
|
|
||||||
|
|
||||||
proc triplesFromJson(j: JsonNode): seq[Triple] =
|
|
||||||
result = newSeq[Triple](j.len)
|
|
||||||
var i = 0
|
|
||||||
for t in j:
|
|
||||||
doAssert t.kind == JArray
|
|
||||||
result[i] = [t[0].str, t[1].str, t[2].str]
|
|
||||||
inc i
|
|
||||||
|
|
||||||
proc messageFromJson(j: JsonNode): Message =
|
|
||||||
Message(kind: MessageKind(j["kind"].num), id: MessageId(j["id"].num),
|
|
||||||
data: triplesFromJson(j["data"]), version: j["version"].num.int)
|
|
||||||
|
|
||||||
var server = newAsyncHttpServer()
|
|
||||||
|
|
||||||
proc cb(req: Request) {.async.} =
|
|
||||||
let (success, error) = await(verifyWebsocketRequest(req, "karaxdb"))
|
|
||||||
if not success:
|
|
||||||
echo "WS negotiation failed: " & error
|
|
||||||
await req.respond(Http400, "Websocket negotiation failed: " & error)
|
|
||||||
req.client.close
|
|
||||||
else:
|
|
||||||
echo "New websocket customer arrived!"
|
|
||||||
while true:
|
|
||||||
try:
|
|
||||||
var f = await req.client.readData(false)
|
|
||||||
echo "(opcode: " & $f.opcode & ", data: " & $f.data.len & ")"
|
|
||||||
let m = messageFromJson(f.data.parseJson)
|
|
||||||
let om = Message(kind: Accepted, id: m.id, data: @[], version: m.version)
|
|
||||||
let oms = $(%*om)
|
|
||||||
echo "OUTPUT ", oms
|
|
||||||
if f.opcode == Opcode.Text:
|
|
||||||
waitFor req.client.sendText(oms, false)
|
|
||||||
else:
|
|
||||||
echo "protocol error"
|
|
||||||
#waitFor req.client.sendBinary(f.data, false)
|
|
||||||
except:
|
|
||||||
echo getCurrentExceptionMsg()
|
|
||||||
break
|
|
||||||
|
|
||||||
req.client.close()
|
|
||||||
echo ".. socket went away."
|
|
||||||
|
|
||||||
waitFor server.serve(Port(8080), cb)
|
|
||||||
151
src/karaxdb/transactor.nim
Normal file
151
src/karaxdb/transactor.nim
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
|
||||||
|
import asynchttpserver, asyncdispatch, asyncnet, "../../../websocket/websocket", common, json,
|
||||||
|
strutils, times
|
||||||
|
|
||||||
|
type
|
||||||
|
Message = object
|
||||||
|
kind: MessageKind
|
||||||
|
id: MessageId
|
||||||
|
data: seq[Triple]
|
||||||
|
version: int
|
||||||
|
|
||||||
|
proc `%`(id: MessageId): JsonNode = %BiggestInt(id)
|
||||||
|
proc `%`(k: MessageKind): JsonNode = %BiggestInt(k)
|
||||||
|
|
||||||
|
proc triplesFromJson(j: JsonNode): seq[Triple] =
|
||||||
|
result = newSeq[Triple](j.len)
|
||||||
|
var i = 0
|
||||||
|
for t in j:
|
||||||
|
doAssert t.kind == JArray
|
||||||
|
let val = if t[2].kind == JNull: string(nil) else: t[2].str
|
||||||
|
result[i] = [t[0].str, t[1].str, val]
|
||||||
|
inc i
|
||||||
|
|
||||||
|
proc messageFromJson(j: JsonNode): Message =
|
||||||
|
Message(kind: MessageKind(j["kind"].num), id: MessageId(j["id"].num),
|
||||||
|
data: triplesFromJson(j["data"]), version: j["version"].num.int)
|
||||||
|
|
||||||
|
proc error(msg: string) = echo msg
|
||||||
|
proc warn(msg: string) = echo msg
|
||||||
|
|
||||||
|
type
|
||||||
|
Tx = object
|
||||||
|
data: string
|
||||||
|
version: int
|
||||||
|
Client = ref object
|
||||||
|
socket: AsyncSocket
|
||||||
|
connected: bool
|
||||||
|
hostname: string
|
||||||
|
lastMessage: float
|
||||||
|
rapidMessageCount: int
|
||||||
|
|
||||||
|
Server = ref object
|
||||||
|
clients: seq[Client]
|
||||||
|
needsUpdate: bool
|
||||||
|
txs: seq[Tx]
|
||||||
|
version: int
|
||||||
|
|
||||||
|
proc newClient(socket: AsyncSocket, hostname: string): Client =
|
||||||
|
Client(socket: socket, connected: true, hostname: hostname)
|
||||||
|
|
||||||
|
proc `$`(client: Client): string =
|
||||||
|
"Client(ip: $1)" % [client.hostname]
|
||||||
|
|
||||||
|
proc updateClients(server: Server) {.async.} =
|
||||||
|
while true:
|
||||||
|
var needsUpdate = false
|
||||||
|
for client in server.clients:
|
||||||
|
if not client.connected:
|
||||||
|
needsUpdate = true
|
||||||
|
break
|
||||||
|
|
||||||
|
server.needsUpdate = server.needsUpdate or needsUpdate
|
||||||
|
if server.needsUpdate and server.txs.len != 0:
|
||||||
|
var someDead = false
|
||||||
|
# perform a copy to prevent the race condition:
|
||||||
|
var txs = server.txs
|
||||||
|
setLen(server.txs, 0)
|
||||||
|
for tx in txs:
|
||||||
|
for c in server.clients:
|
||||||
|
if c.connected:
|
||||||
|
await c.socket.sendText(tx.data, false)
|
||||||
|
else:
|
||||||
|
someDead = true
|
||||||
|
if someDead:
|
||||||
|
var i = 0
|
||||||
|
while i < server.clients.len:
|
||||||
|
if not server.clients[i].connected: del(server.clients, i)
|
||||||
|
else: inc i
|
||||||
|
server.needsUpdate = false
|
||||||
|
# let other stuff in the main loop run:
|
||||||
|
await sleepAsync(10)
|
||||||
|
|
||||||
|
proc processMessage(server: Server, client: Client, data: string) {.async.} =
|
||||||
|
# Check if last message was relatively recent. If so, kick the user.
|
||||||
|
echo "processMessage ", data
|
||||||
|
if epochTime() - client.lastMessage < 0.1: # 100ms
|
||||||
|
client.rapidMessageCount.inc
|
||||||
|
else:
|
||||||
|
client.rapidMessageCount = 0
|
||||||
|
|
||||||
|
client.lastMessage = epochTime()
|
||||||
|
if client.rapidMessageCount > 10:
|
||||||
|
warn("Client ($1) is firing messages too rapidly. Killing." % $client)
|
||||||
|
client.connected = false
|
||||||
|
let msgj = parseJson(data)
|
||||||
|
let msg = messageFromJson(msgj)
|
||||||
|
case msg.kind
|
||||||
|
of Newdata:
|
||||||
|
if msg.version == server.version:
|
||||||
|
server.txs.add Tx(data: data, version: msg.version)
|
||||||
|
server.needsUpdate = true
|
||||||
|
inc server.version
|
||||||
|
else:
|
||||||
|
let om = Message(kind: Rejected, id: msg.id, data: @[], version: server.version)
|
||||||
|
await client.socket.sendText($(%*om), false)
|
||||||
|
else:
|
||||||
|
# either Disconnect or an invalid message type:
|
||||||
|
client.connected = false
|
||||||
|
server.needsUpdate = true
|
||||||
|
|
||||||
|
proc processClient(server: Server, client: Client) {.async.} =
|
||||||
|
while client.connected:
|
||||||
|
var frameFut = client.socket.readData(false)
|
||||||
|
yield frameFut
|
||||||
|
if frameFut.failed:
|
||||||
|
error("Error occurred handling client messages.\n" &
|
||||||
|
frameFut.error.msg)
|
||||||
|
client.connected = false
|
||||||
|
break
|
||||||
|
|
||||||
|
let frame = frameFut.read()
|
||||||
|
if frame.opcode == Opcode.Text:
|
||||||
|
let processFut = processMessage(server, client, frame.data)
|
||||||
|
if processFut.failed:
|
||||||
|
error("Client ($1) attempted to send bad JSON? " % $client & "\n" &
|
||||||
|
processFut.error.msg)
|
||||||
|
client.connected = false
|
||||||
|
|
||||||
|
client.socket.close()
|
||||||
|
|
||||||
|
proc onRequest(server: Server, req: Request) {.async.} =
|
||||||
|
let (success, error) = await verifyWebsocketRequest(req, "karaxdb")
|
||||||
|
if success:
|
||||||
|
echo("Client connected from ", req.hostname)
|
||||||
|
server.clients.add(newClient(req.client, req.hostname))
|
||||||
|
asyncCheck processClient(server, server.clients[^1])
|
||||||
|
else:
|
||||||
|
echo("WS negotiation failed: ", error)
|
||||||
|
await req.respond(Http400, "WebSocket negotiation failed: " & error)
|
||||||
|
req.client.close()
|
||||||
|
|
||||||
|
proc main =
|
||||||
|
let httpServer = newAsyncHttpServer()
|
||||||
|
let server = Server(clients: @[], txs: @[])
|
||||||
|
|
||||||
|
proc cb(req: Request): Future[void] {.async.} = await onRequest(server, req)
|
||||||
|
|
||||||
|
asyncCheck updateClients(server)
|
||||||
|
waitFor httpServer.serve(Port(8080), cb)
|
||||||
|
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue