git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: b2886f60b5b31141643c0ea134c62b9e8e0db8e2

Files: b2886f60b5b31141643c0ea134c62b9e8e0db8e2 / lib / persistent-gossip / schedule.js

6475 bytesRaw
1var ip = require('ip')
2var onWakeup = require('on-wakeup')
3var onNetwork = require('on-change-network')
4var hasNetwork = require('has-network')
5
6var pull = require('pull-stream')
7
8function not (fn) {
9 return function (e) { return !fn(e) }
10}
11
12function and () {
13 var args = [].slice.call(arguments)
14 return function (value) {
15 return args.every(function (fn) { return fn.call(null, value) })
16 }
17}
18
19//min delay (delay since last disconnect of most recent peer in unconnected set)
20//unconnected filter delay peer < min delay
21function delay (failures, factor, max) {
22 return Math.min(Math.pow(2, failures)*factor, max || Infinity)
23}
24
25function maxStateChange (M, e) {
26 return Math.max(M, e.stateChange || 0)
27}
28
29function peerNext(peer, opts) {
30 return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
31}
32
33
34//detect if not connected to wifi or other network
35//(i.e. if there is only localhost)
36
37function isOffline (e) {
38 if(ip.isLoopback(e.host)) return false
39 return !hasNetwork()
40}
41
42var isOnline = not(isOffline)
43
44function isLocal (e) {
45 // don't rely on private ip address, because
46 // cjdns creates fake private ip addresses.
47 return ip.isPrivate(e.host) && e.source === 'local'
48}
49
50function isFriend (e) {
51 return e.source === 'friends'
52}
53
54function isUnattempted (e) {
55 return !e.stateChange
56}
57
58//select peers which have never been successfully connected to yet,
59//but have been tried.
60function isInactive (e) {
61 return e.stateChange && (!e.duration || e.duration.mean == 0)
62}
63
64function isLongterm (e) {
65 return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
66}
67
68//peers which we can connect to, but are not upgraded.
69//select peers which we can connect to, but are not upgraded to LT.
70//assume any peer is legacy, until we know otherwise...
71function isLegacy (peer) {
72 return peer.duration && peer.duration.mean > 0 && !exports.isLongterm(peer)
73}
74
75function isConnect (e) {
76 return 'connected' === e.state || 'connecting' === e.state
77}
78
79//sort oldest to newest then take first n
80function earliest(peers, n) {
81 return peers.sort(function (a, b) {
82 return a.stateChange - b.stateChange
83 }).slice(0, Math.max(n, 0))
84}
85
86function select(peers, ts, filter, opts) {
87 if(opts.disable) return []
88 //opts: { quota, groupMin, min, factor, max }
89 var type = peers.filter(filter)
90 var unconnect = type.filter(not(isConnect))
91 var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
92 var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
93 if(ts < min) return []
94
95 return earliest(unconnect.filter(function (peer) {
96 return peerNext(peer, opts) < ts
97 }), count)
98}
99
100var schedule = exports = module.exports =
101function (gossip, config, server) {
102// return
103 var min = 60e3, hour = 60*60e3
104
105 //trigger hard reconnect after suspend or local network changes
106 onWakeup(gossip.reconnect)
107 onNetwork(gossip.reconnect)
108
109 function conf(name, def) {
110 if(!config.gossip) return def
111 var value = config.gossip[name]
112 return (value === undefined || value === '') ? def : value
113 }
114
115 function connect (peers, ts, name, filter, opts) {
116 opts.group = name
117 var connected = peers.filter(isConnect).filter(filter)
118
119 //disconnect if over quota
120 if(connected.length > opts.quota) {
121 return earliest(connected, connected.length - opts.quota)
122 .forEach(function (peer) {
123 gossip.disconnect(peer)
124 })
125 }
126
127 //will return [] if the quota is full
128 var selected = select(peers, ts, and(filter, isOnline), opts)
129 selected
130 .forEach(function (peer) {
131 gossip.connect(peer)
132 })
133 }
134
135
136 var connecting = false
137 function connections () {
138 if(connecting) return
139 connecting = true
140 setTimeout(function () {
141 connecting = false
142 var ts = Date.now()
143 var peers = gossip.peers()
144
145 var connected = peers.filter(and(isConnect, not(isLocal), not(isFriend))).length
146 var connectedFriends = peers.filter(and(isConnect, isFriend)).length
147
148 connect(peers, ts, 'local', exports.isLocal, {
149 quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
150 disable: !conf('local', true)
151 })
152
153 // prioritize friends
154 connect(peers, ts, 'friends', and(exports.isFriend, exports.isLongterm), {
155 quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
156 disable: !conf('local', true)
157 })
158
159 if (connectedFriends < 2)
160 connect(peers, ts, 'attemptFriend', and(exports.isFriend, exports.isUnattempted), {
161 min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
162 disable: !conf('global', true)
163 })
164
165 connect(peers, ts, 'retryFriends', and(exports.isFriend, exports.isInactive), {
166 min: 0,
167 quota: 3, factor: 60e3, max: 3*60*60e3, groupMin: 5*60e3
168 })
169
170 // standard longterm peers
171 connect(peers, ts, 'longterm', and(
172 exports.isLongterm,
173 not(exports.isFriend),
174 not(exports.isLocal)
175 ), {
176 quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
177 disable: !conf('global', true)
178 })
179
180 if(!connected)
181 connect(peers, ts, 'attempt', exports.isUnattempted, {
182 min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
183 disable: !conf('global', true)
184 })
185
186 //quota, groupMin, min, factor, max
187 connect(peers, ts, 'retry', exports.isInactive, {
188 min: 0,
189 quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3
190 })
191
192 var longterm = peers.filter(isConnect).filter(exports.isLongterm).length
193
194 connect(peers, ts, 'legacy', exports.isLegacy, {
195 quota: 3 - longterm,
196 factor: 5*min, max: 3*hour, groupMin: 5*min,
197 disable: !conf('global', true)
198 })
199
200 peers.filter(isConnect).forEach(function (e) {
201 var permanent = exports.isLongterm(e) || exports.isLocal(e)
202 if((!permanent || e.state === 'connecting') && e.stateChange + 10e3 < ts) {
203 gossip.disconnect(e)
204 }
205 })
206
207 }, 100*Math.random())
208
209 }
210
211 pull(
212 gossip.changes(),
213 pull.drain(function (ev) {
214 if(ev.type == 'disconnect')
215 connections()
216 })
217 )
218
219 var int = setInterval(connections, 2e3)
220 if(int.unref) int.unref()
221
222 connections()
223
224}
225
226exports.isUnattempted = isUnattempted
227exports.isInactive = isInactive
228exports.isLongterm = isLongterm
229exports.isLegacy = isLegacy
230exports.isLocal = isLocal
231exports.isFriend = isFriend
232exports.isConnectedOrConnecting = isConnect
233exports.select = select
234

Built with git-ssb-web