Shirish Agarwal: Anti-national says the Indian Law Minister.
Anti-national and Anti-India judges Kiren Rijiju, Law Minister.

There is much to share but some other day, Till later.
Anti-national and Anti-India judges Kiren Rijiju, Law Minister.

There is much to share but some other day, Till later.
| Series: | Fall Revolution #1 |
| Publisher: | Orbit |
| Copyright: | 1995 |
| Printing: | 2001 |
| ISBN: | 1-85723-833-8 |
| Format: | Trade paperback |
| Pages: | 341 |
| Publisher: | The New Press |
| Copyright: | 2022 |
| ISBN: | 1-62097-690-0 |
| Format: | Kindle |
| Pages: | 257 |
SELECT * FROM ( SELECT toStartOfMinute(TimeReceived) AS TimeReceived, DstAddr, SrcPort, dictGetOrDefault('protocols', 'name', Proto, '???') AS Proto, SUM(((((Bytes * SamplingRate) * 8) / 1000) / 1000) / 1000) / 60 AS Gbps, uniq(SrcAddr) AS sources, uniq(SrcCountry) AS countries FROM flows WHERE TimeReceived > now() - INTERVAL 5 MINUTE AND DstNetRole = 'customers' GROUP BY TimeReceived, DstAddr, SrcPort, Proto ) WHERE (Gbps > 1) OR ((Proto = 'UDP') AND (Gbps > 0.2)) OR ((sources > 20) AND (Gbps > 0.1)) OR ((countries > 10) AND (Gbps > 0.1)) ORDER BY TimeReceived DESC, Gbps DESC
| TimeReceived | DstAddr | SrcPort | Proto | Gbps | sources | countries |
|---|---|---|---|---|---|---|
| 2023-02-26 17:44:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.102 | 109 | 13 |
| 2023-02-26 17:43:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.130 | 133 | 17 |
| 2023-02-26 17:43:00 | ::ffff:203.0.113.68 |
53 | UDP | 0.129 | 364 | 63 |
| 2023-02-26 17:43:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.113 | 129 | 21 |
| 2023-02-26 17:42:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.139 | 50 | 14 |
| 2023-02-26 17:42:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.105 | 42 | 14 |
| 2023-02-26 17:40:00 | ::ffff:203.0.113.68 |
53 | UDP | 0.121 | 340 | 65 |
65535:666 for this purpose. This is known as a
remote-triggered blackhole (RTBH) and is explained in more detail in RFC 3882.
It is also possible to blackhole the source of the attacks by leveraging
unicast Reverse Path Forwarding (uRPF) from RFC 3704, as explained in RFC 5635. However, uRPF can be a serious tax on your router resources. See
NCS5500 uRPF: Configuration and Impact on Scale for an example of the kind
of restrictions you have to expect when enabling uRPF.
On the advertising side, we can use BIRD. Here is a complete configuration
file to allow any router to collect them:
log stderr all; router id 192.0.2.1; protocol device scan time 10; protocol bgp exporter ipv4 import none; export where proto = "blackhole4"; ; ipv6 import none; export where proto = "blackhole6"; ; local as 64666; neighbor range 192.0.2.0/24 external; multihop; dynamic name "exporter"; dynamic name digits 2; graceful restart yes; graceful restart time 0; long lived graceful restart yes; long lived stale time 3600; # keep routes for 1 hour! protocol static blackhole4 ipv4; route 203.0.113.206/32 blackhole bgp_community.add((65535, 666)); ; route 203.0.113.68/32 blackhole bgp_community.add((65535, 666)); ; protocol static blackhole6 ipv6;
router static vrf public address-family ipv4 unicast 192.0.2.1/32 Null0 description "BGP blackhole" ! address-family ipv6 unicast 2001:db8::1/128 Null0 description "BGP blackhole" ! ! ! route-policy blackhole_ipv4_in_public if destination in (0.0.0.0/0 le 31) then drop endif set next-hop 192.0.2.1 done end-policy ! route-policy blackhole_ipv6_in_public if destination in (::/0 le 127) then drop endif set next-hop 2001:db8::1 done end-policy ! router bgp 12322 neighbor-group BLACKHOLE_IPV4_PUBLIC remote-as 64666 ebgp-multihop 255 update-source Loopback10 address-family ipv4 unicast maximum-prefix 100 90 route-policy blackhole_ipv4_in_public in route-policy drop out long-lived-graceful-restart stale-time send 86400 accept 86400 ! address-family ipv6 unicast maximum-prefix 100 90 route-policy blackhole_ipv6_in_public in route-policy drop out long-lived-graceful-restart stale-time send 86400 accept 86400 ! ! vrf public neighbor 192.0.2.1 use neighbor-group BLACKHOLE_IPV4_PUBLIC description akvorado-1
ForwardingStatus >= 128 as a filter.
While this method is compatible with all routers, it makes the attack successful
as the target is completely unreachable. If your router supports it, Flowspec
can selectively filter flows to stop the attack without impacting the
customer.
flow4 table flowtab4; flow6 table flowtab6; protocol bgp exporter flow4 import none; export where proto = "flowspec4"; ; flow6 import none; export where proto = "flowspec6"; ; # [ ] protocol static flowspec4 flow4; route flow4 dst 203.0.113.68/32; sport = 53; length >= 1476 && <= 1500; proto = 17; bgp_ext_community.add((generic, 0x80060000, 0x00000000)); ; route flow4 dst 203.0.113.206/32; sport = 123; length = 468; proto = 17; bgp_ext_community.add((generic, 0x80060000, 0x00000000)); ; protocol static flowspec6 flow6;
vrf public address-family ipv4 flowspec address-family ipv6 flowspec ! router bgp 12322 address-family vpnv4 flowspec address-family vpnv6 flowspec neighbor-group FLOWSPEC_IPV4_PUBLIC remote-as 64666 ebgp-multihop 255 update-source Loopback10 address-family ipv4 flowspec long-lived-graceful-restart stale-time send 86400 accept 86400 route-policy accept in route-policy drop out maximum-prefix 100 90 validation disable ! address-family ipv6 flowspec long-lived-graceful-restart stale-time send 86400 accept 86400 route-policy accept in route-policy drop out maximum-prefix 100 90 validation disable ! ! vrf public address-family ipv4 flowspec address-family ipv6 flowspec neighbor 192.0.2.1 use neighbor-group FLOWSPEC_IPV4_PUBLIC description akvorado-1
flowspec vrf public address-family ipv4 local-install interface-all ! address-family ipv6 local-install interface-all ! ! !
ForwardingStatus >=
128.
route flow4 dst 203.0.113.68/32; sport = 53; length >= 1476 && <= 1500; proto = 17; bgp_ext_community.add((generic, 0x80060000, 0x00000000)); ;
quantiles(0.1,
0.9)(Bytes/Packets).
The last issue we need to tackle is how to optimize the request: it may need
several seconds to collect the data and it is likely to consume substantial
resources from your ClickHouse database. One solution is to create a
materialized view to pre-aggregate results:
CREATE TABLE ddos_logs ( TimeReceived DateTime, DstAddr IPv6, Proto UInt32, SrcPort UInt16, Gbps SimpleAggregateFunction(sum, Float64), Mpps SimpleAggregateFunction(sum, Float64), sources AggregateFunction(uniqCombined(12), IPv6), countries AggregateFunction(uniqCombined(12), FixedString(2)), size AggregateFunction(quantiles(0.1, 0.9), UInt64) ) ENGINE = SummingMergeTree PARTITION BY toStartOfHour(TimeReceived) ORDER BY (TimeReceived, DstAddr, Proto, SrcPort) TTL toStartOfHour(TimeReceived) + INTERVAL 6 HOUR DELETE ; CREATE MATERIALIZED VIEW ddos_logs_view TO ddos_logs AS SELECT toStartOfMinute(TimeReceived) AS TimeReceived, DstAddr, Proto, SrcPort, sum(((((Bytes * SamplingRate) * 8) / 1000) / 1000) / 1000) / 60 AS Gbps, sum(((Packets * SamplingRate) / 1000) / 1000) / 60 AS Mpps, uniqCombinedState(12)(SrcAddr) AS sources, uniqCombinedState(12)(SrcCountry) AS countries, quantilesState(0.1, 0.9)(toUInt64(Bytes/Packets)) AS size FROM flows WHERE DstNetRole = 'customers' GROUP BY TimeReceived, DstAddr, Proto, SrcPort
ddos_logs table is using the SummingMergeTree engine. When the table
receives new data, ClickHouse replaces all the rows with the same sorting key,
as defined by the ORDER BY directive, with one row which contains summarized
values using either the sum() function or the explicitly specified aggregate
function (uniqCombined and quantiles in our example).3
Finally, we can modify our initial query with the following one:
SELECT * FROM ( SELECT TimeReceived, DstAddr, dictGetOrDefault('protocols', 'name', Proto, '???') AS Proto, SrcPort, sum(Gbps) AS Gbps, sum(Mpps) AS Mpps, uniqCombinedMerge(12)(sources) AS sources, uniqCombinedMerge(12)(countries) AS countries, quantilesMerge(0.1, 0.9)(size) AS size FROM ddos_logs WHERE TimeReceived > now() - INTERVAL 60 MINUTE GROUP BY TimeReceived, DstAddr, Proto, SrcPort ) WHERE (Gbps > 1) OR ((Proto = 'UDP') AND (Gbps > 0.2)) OR ((sources > 20) AND (Gbps > 0.1)) OR ((countries > 10) AND (Gbps > 0.1)) ORDER BY TimeReceived DESC, Gbps DESC
SummingMergeTree tables,import socket import types from clickhouse_driver import Client as CHClient # Put your SQL query here! SQL_QUERY = " " # How many anti-DDoS rules we want at the same time? MAX_DDOS_RULES = 20 def empty_ruleset(): ruleset = types.SimpleNamespace() ruleset.flowspec = types.SimpleNamespace() ruleset.blackhole = types.SimpleNamespace() ruleset.flowspec.v4 = [] ruleset.flowspec.v6 = [] ruleset.blackhole.v4 = [] ruleset.blackhole.v6 = [] return ruleset current_ruleset = empty_ruleset() client = CHClient(host="clickhouse.akvorado.net") while True: results = client.execute(SQL_QUERY) seen = new_ruleset = empty_ruleset() for (t, addr, proto, port, gbps, mpps, sources, countries, size) in results: if (addr, proto, port) in seen: continue seen[(addr, proto, port)] = True # Flowspec if addr.ipv4_mapped: address = addr.ipv4_mapped rules = new_ruleset.flowspec.v4 table = "flow4" mask = 32 nh = "proto" else: address = addr rules = new_ruleset.flowspec.v6 table = "flow6" mask = 128 nh = "next header" if size[0] == size[1]: length = f"length = int(size[0]) " else: length = f"length >= int(size[0]) && <= int(size[1]) " header = f""" # Time: t # Source: address , protocol: proto , port: port # Gbps/Mpps: gbps:.3 / mpps:.3 , packet size: int(size[0]) <=X<= int(size[1]) # Flows: flows , sources: sources , countries: countries """ rules.append( f""" header route table dst address / mask ; sport = port ; length ; nh = socket.getprotobyname(proto) ; bgp_ext_community.add((generic, 0x80060000, 0x00000000)); ; """ ) # Blackhole if addr.ipv4_mapped: rules = new_ruleset.blackhole.v4 else: rules = new_ruleset.blackhole.v6 rules.append( f""" header route address / mask blackhole bgp_community.add((65535, 666)); ; """ ) new_ruleset.flowspec.v4 = list( set(new_ruleset.flowspec.v4[:MAX_DDOS_RULES]) ) new_ruleset.flowspec.v6 = list( set(new_ruleset.flowspec.v6[:MAX_DDOS_RULES]) ) # TODO: advertise changes by mail, chat, ... current_ruleset = new_ruleset changes = False for rules, path in ( (current_ruleset.flowspec.v4, "v4-flowspec"), (current_ruleset.flowspec.v6, "v6-flowspec"), (current_ruleset.blackhole.v4, "v4-blackhole"), (current_ruleset.blackhole.v6, "v6-blackhole"), ): path = os.path.join("/etc/bird/", f" path .conf") with open(f" path .tmp", "w") as f: for r in rules: f.write(r) changes = ( changes or not os.path.exists(path) or not samefile(path, f" path .tmp") ) os.rename(f" path .tmp", path) if not changes: continue proc = subprocess.Popen( ["birdc", "configure"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout, stderr = proc.communicate(None) stdout = stdout.decode("utf-8", "replace") stderr = stderr.decode("utf-8", "replace") if proc.returncode != 0: logger.error( " error:\n \n ".format( "birdc reconfigure", "\n".join( [" O: ".format(line) for line in stdout.rstrip().split("\n")] ), "\n".join( [" E: ".format(line) for line in stderr.rstrip().split("\n")] ), ) )
FORMAT Markdown to the query.
Icy morning Witch Wells Azparser.peg. Here is a simplified rule:
ConditionIPExpr "condition on IP" column:("ExporterAddress"i return "ExporterAddress", nil / "SrcAddr"i return "SrcAddr", nil / "DstAddr"i return "DstAddr", nil ) _ operator:("=" / "!=") _ ip:IP return fmt.Sprintf("%s %s IPv6StringToNum(%s)", toString(column), toString(operator), quote(ip)), nil
ConditionIPExpr. It case-insensitively matches
ExporterAddress, SrcAddr, or DstAddr. The action for each case returns the
proper case for the column name. That s what is stored in the column variable.
Then, it matches one of the possible operators. As there is no code block, it
stores the matched string directly in the operator variable. Then, it tries to
match the IP rule, which is defined elsewhere in the grammar. If it succeeds,
it stores the result of the match in the ip variable and executes the final
action. The action turns the column, operator, and IP into a proper expression
for ClickHouse. For example, if we have ExporterAddress = 203.0.113.15, we
get ExporterAddress = IPv6StringToNum('203.0.113.15').
The IP rule uses a rudimentary regular expression but checks if the matched
address is correct in the action block, thanks to netip.ParseAddr():
IP "IP address" [0-9A-Fa-f:.]+ ip, err := netip.ParseAddr(string(c.text)) if err != nil return "", errors.New("expecting an IP address") return ip.String(), nil
WHERE clause accepted by
ClickHouse:3
WHERE InIfBoundary = 'external' AND ExporterRegion = 'france' AND InIfConnectivity = 'transit' AND SrcAS = 15169 AND DstAddr BETWEEN toIPv6('2a01:e0f:ffff::') AND toIPv6('2a01:e0f:ffff:ffff:ffff:ffff:ffff:ffff')
InputFilter, uses CodeMirror as its foundation and
leverages features such as syntax highlighting, linting, and completion. The
source code for these capabilities can be found in the
codemirror/lang-filter/ directory.
@top Filter expression expression Not expression "(" expression ")" "(" expression ")" And expression "(" expression ")" Or expression comparisonExpression And expression comparisonExpression Or expression comparisonExpression comparisonExpression Column Operator Value Value String Literal ValueLParen ListOfValues ValueRParen ListOfValues ListOfValues ValueComma (String Literal) String Literal // [ ] @tokens // [ ] Column std.asciiLetter (std.asciiLetter std.digit)* Operator $[a-zA-Z!=><]+ String '"' (![\\\n"] "\\" _)* '"'? "'" (![\\\n'] "\\" _)* "'"? Literal (std.digit std.asciiLetter $[.:/])+ // [ ]
SrcAS = 12322 AND (DstAS = 1299 OR SrcAS = 29447) is parsed to:
Filter(Column, Operator, Value(Literal), And, Column, Operator, Value(Literal), Or, Column, Operator, Value(Literal))
export const FilterLanguage = LRLanguage.define( parser: parser.configure( props: [ styleTags( Column: t.propertyName, String: t.string, Literal: t.literal, LineComment: t.lineComment, BlockComment: t.blockComment, Or: t.logicOperator, And: t.logicOperator, Not: t.logicOperator, Operator: t.compareOperator, "( )": t.paren, ), ], ), );
/api/v0/console/filter/validate endpoint accepts a filter and returns a JSON
structure with the errors that were found:
"message": "at line 1, position 12: string literal not terminated", "errors": [ "line": 1, "column": 12, "offset": 11, "message": "string literal not terminated", ]
/api/v0/console/filter/complete endpoint.
Walking the syntax tree was not as easy as I thought, but unit tests helped
a lot.
The backend uses the parser generated by pigeon to complete a column name
or a comparison operator. For values, the completions are either static or
extracted from the ClickHouse database. A user can complete an AS number from
an organization name thanks to the following snippet:
results := []struct Label string ch:"label" Detail string ch:"detail" columnName := "DstAS" sqlQuery := fmt.Sprintf( SELECT concat('AS', toString(%s)) AS label, dictGet('asns', 'name', %s) AS detail FROM flows WHERE TimeReceived > date_sub(minute, 1, now()) AND detail != '' AND positionCaseInsensitive(detail, $1) >= 1 GROUP BY label, detail ORDER BY COUNT(*) DESC LIMIT 20 , columnName, columnName) if err := conn.Select(ctx, &results, sqlQuery, input.Prefix); err != nil c.r.Err(err).Msg("unable to query database") break for _, result := range results completions = append(completions, filterCompletion Label: result.Label, Detail: result.Detail, Quoted: false, )
It's been a year since I started exploring HLedger, and I'm still
going. The rollover to 2023 was an opportunity to revisit my approach.
Some time ago I stumbled across Dmitry Astapov's HLedger notes (fully-fledged
hledger, which I briefly
mentioned in eventual consistency) and decided to adopt some of its ideas.
new year, new journal
First up, Astapov encourages starting a new journal file for a new calendar
year. I do this for other, accounting-adjacent files as a matter of course,
and I did it for my GNUCash files prior to adopting HLedger. But the reason
for those is a general suspicion that a simple mistake with those softwares
could irrevocably corrupt my data. I'm much more confident with HLedger, so
rolling over at years end isn't necessary for that. But there are other
advantages. A quick obvious one is you can get rid of old accounts (such as
expense accounts tied to a particular project, now completed).
one journal per import
In the first year, I periodically imported account data via CSV exports
of transactions and HLedger's (excellent) CSV import system. I imported
all the transactions, once each, into a single, large journal file.
Astapov instead advocates for creating a separate journal
for each CSV that you wish to import, and keep around the CSV, leaving you
with a 1:1 mapping of CSV:journal. Then use HLedger's "include" mechanism to
pull them all into the main journal.
With the former approach, where the CSV data was imported precisely, once, it
was only exposed to your import rules once. The workflow ended up being:
import transactions; notice some that you could have matched with import rules
and auto-coded; write the rule for the next time. With Astapov's approach, you
can re-generate the journal from the CSV at any point in the future with an
updated set of import rules.
tracking dependencies
Now we get onto the job of driving the generation of all these derivative
journal files. Astapov has built a sophisticated system using Haskell's "Shake",
which I'm not yet familiar, but for my sins I'm quite adept at (GNU-flavoured)
UNIX Make, so I started building with that. An example rule
import/jon/amex/%.journal: import/jon/amex/%.csv rules/amex.csv.rules
rm -f $(@D)/.latest.$*.csv $@
hledger import --rules-file rules/amex.csv.rules -f $@ $<
This captures the dependency between the journal and the underlying CSV
but also to the relevant rules file; if I modify that, and this target
is run in the future, all dependent journals should be re-generated.1
opening balances
It's all fine and well starting over in a new year, and I might be generous
to forgive debts, but I can't count on others to do the same. We need
to carry over some balance information from one year to the next. Astapov has
a more complex (or perhaps featureful) scheme for this involving a custom
Haskell program, but I bodged something with a pair of make targets:
import/opening/2023.csv: 2022.journal
mkdir -p import/opening
hledger bal -f $< \
$(list_of_accounts_I_want_to_carry_over) \
-O csv -N > $@
import/opening/2023.journal: import/opening/2023.csv rules/opening.rules
rm -f $(@D)/.latest.2023.csv $@
hledger import --rules-file rules/opening.rules \
-f $@ $<
I think this could be golfed into a year-generic rule with a little more work.
The nice thing about this approach is the opening balances for a given year
might change, if adjustments are made in prior years. They shouldn't, for
real accounts, but very well could for more "virtual" liabilities. (including:
deciding to write off debts.)
run lots of reports
Astapov advocates for running lots of reports, and automatically. There's a
really obvious advantage of that to me: there's no chance anyone except me
will actually interact with HLedger itself. For family finances, I need
reports to be able to discuss anything with my wife.
Extending my make rules to run reports is trivial. I've gone for HTML
reports for the most part, as they're the easiest on the eye. Unfortunately
the most useful report to discuss (at least at the moment) would be a list
of transactions in a given expense category, and the register/aregister
commands did not support HTML as an output format. I submitted my first
HLedger patch to add HTML output support to aregister:
https://github.com/simonmichael/hledger/pull/2000
addressing the virtual posting problem
I wrote in my original hledger blog post that I had to resort to
unbalanced virtual postings in order to record both a liability between
my personal cash and family, as well as categorise the spend. I still
haven't found a nice way around that.
But I suspect having broken out the journal into lots of other journals
paves the way to a better solution to the above.
The form of a solution I am thinking of is: some scheme whereby the two
destination accounts are combined together; perhaps, choose one as a primary
and encode the other information in sub-accounts under that. For example,
repeating the example from my hledger blog post:
2022-01-02 ZTL*RELISH
family:liabilities:creditcard -3.00
family:dues:jon 3.00
(jon:expenses:snacks) 3.00
This could become
2022-01-02 ZTL*RELISH
family:liabilities:creditcard -3.00
family:liabilities:jon:snacks
(I note this is very similar to a solution proposed to me by someone
responding on twitter).
The next step is to recognise that sometimes when looking at the data I
care about one aspect, and at other times the other, but rarely both. So
for the case where I'm thinking about family finances, I could use
account aliases
to effectively flatten out the expense category portion and ignore it.
On the other hand, when I'm concerned about how I've spent my personal
cash and not about how much I owe the family account, I could use
aliases to do the opposite: rewrite-away the family:liabilities:jon
prefix and combine the transactions with the regular jon:expenses
account heirarchy.
(this is all speculative: I need to actually try this.)
catching errors after an import
When I import the transactions for a given real bank account, I check the
final balance against another source: usually a bank statement, to make
sure they agree. I wasn't using any of the myriad methods to make sure
that this remains true later on, and so there was the risk that I make an
edit to something and accidentally remove a transaction that contributed
to that number, and not notice (until the next import).
The CSV data my bank gives me for accounts (not for credit cards) also includes
a 'resulting balance' field. It was therefore trivial to extend the CSV import
rules to add balance
assertions to
the transactions that are generated. This catches the problem.
There are a couple of warts with balance assertions on every such
transaction: for example, dealing with the duplicate transaction for paying
a credit card: one from the bank statement, one from the credit card.
Removing one of the two is sufficient to correct the account balances but
sometimes they don't agree on the transaction date, or the transactions
within a given day are sorted slightly differently by HLedger than by the
bank. The simple solution is to just manually delete one or two assertions:
there remain plenty more for assurance.
going forward
I've only scratched the surface of the suggestions in Astapov's "full fledged
HLedger" notes. I'm up to step 2 of 14. I'm expecting to return to it once
the changes I've made have bedded in a little bit.
I suppose I could anonymize and share the framework (Makefile etc) that I am
using if anyone was interested. It would take some work, though, so I don't know
when I'd get around to it.
.ikiwiki directory of the site
in the hope of making things a little more
"standalone". Unfortunately, that didn't work either because the
theme must be shipped in the system-wide location: I couldn't figure
out how to put it to have it bundled with the main repository. At that
point I mostly gave up because I had spent too much time on this and I
had to do something about email otherwise it would start to bounce.
rsync'd my build in /var/www/html and boom, I had a website. The
Goatcounter analytics were timing out, but that was easy to turn
off.
rsync -v -n --files-from=<(ssh colette.anarc.at find Maildir -name '*colette*' ) colette.anarc.at: colette/
rsync -v -n --files-from=<(ssh colette.anarc.at find Maildir -name '*colette*' ) colette/ marcos.anarc.at:
Overall, the outage lasted about 24 hours, from 11:00EST (16:00UTC) on
2023-02-07 to the same time today.
derive macro without having to mess with procedural macros?
Now you can!
derive-adhoc lets you write a #[derive] macro, using a template syntax which looks a lot like macro_rules!.
It s still 0.x - so unstable, and maybe with sharp edges. We want feedback!
And, the documentation is still very terse. It is doesn t omit anything, but, it is severely lacking in examples, motivation, and so on. It will suit readers who enjoy dense reference material.
( Read more... )
In one week from now, Twitter will block free API access. This prevents anyone who has written interesting bot accounts, integrations, or tooling from accessing Twitter without paying for it. A whole number of fascinating accounts will cease functioning, people will no longer be able to use tools that interact with Twitter, and anyone using a free service to do things like find Twitter mutuals who have moved to Mastodon or to cross-post between Twitter and other services will be blocked.
Welcome to the 39th post in the relatively randomly recurring rants,
or R4 for short. Today s
post picks up where the previous post #38:
Faster Feedback Systems started. As we argued in #38,
the need for fast feedback loops is fairly universal and
widespread. Fairly shortly after we posted #38,
useR! 2022 happened and one presentation had the key line
Waiting 1-24 minutes for a build to finish can be a massive time suck.which we would like to come back to today. Furthermore, the unimitable @b0rk had a fabulous tweet just weeks later stating the same point debugging strategy: shorten your feedback loop as a key in a successful debugging strategy. So in sum: shorter is better. Nobody likes to wait. And shorter i.e. faster is a key and recurrent theme in the R4 series. Today we have a fairly nice illustration of two aspects we have stressed before:
lim-tiny
and lim-tidy.
Our conjecture was (and is!) that less is more just as post
#34
titled Less Is More argued with respect to package
dependencies. Each of the repos just does one thing: a query to a
(freely accessible but remote) PostgreSQL database. The tiny version
just retrieves a data frame using only the dependencies needed for RPostgreSQL
namely DBI and
nothing else. The tidy version retrieves a tibble and has access to
everything else that comes when installing RPostgres: DBI, dplyr, and magrittr plus
of course their respective dependencies. We were able to let the code
run in (very default) GitHub Actions on a weekly schedule without
intervention apart from one change to the SQL query when the remote
server (providing public bioinformatics data) changed its schema
slighly, plus one update to the action yaml code version. No other
changes.
We measure the time a standard continuous integration run takes in
total using the default GitHub Action setup in the tidy case (which
relies on RSPM/PPM binaries, caching, , and does not rebuild
from source each time), and our own r-ci script (introduced
in #32
for CI with R. It switched to using r2u during the course of
this experiment but already had access to binaries via c2d4u
so it is always binaries-based (see e.g. #37
or #13
for more). The chart shows the evolution of these run-times over the
course of the year with one weekly run each.
tiny vs tidy ci timing impact
chart
This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. Please report excessive re-aggregation in third-party for-profit settings.
For the fourth year in a row, I've asked Soci t de Transport de Montr al,
Montreal's transit agency, for the foot traffic data of Montreal's subway.
By clicking on a subway station, you'll be redirected to a graph of the
station's foot traffic.