1use std::sync::Arc;
25use std::time::{Duration, Instant};
26
27use mz_adapter_types::dyncfgs::CATALOG_INFO_METRICS_RECONCILE_INTERVAL;
28use mz_catalog::memory::objects::{
29 CatalogItem, Cluster, ClusterReplica, ClusterVariant, DataSourceDesc,
30};
31use mz_controller::clusters::ReplicaLocation;
32use mz_ore::cast::CastFrom;
33use mz_ore::metric;
34use mz_ore::metrics::{
35 DeleteOnDropGauge, Histogram, MetricTag, MetricVisibility, MetricsRegistry, UIntGaugeVec,
36};
37use mz_ore::stats::histogram_seconds_buckets;
38use mz_ore::task;
39use mz_ore::tracing::OpenTelemetryContext;
40use mz_repr::CatalogItemId;
41use mz_sql::names::{FullItemName, RawDatabaseSpecifier};
42use prometheus::core::AtomicU64;
43use tokio::sync::oneshot;
44use tracing::{debug, warn};
45
46use crate::catalog::{Catalog, catalog_type_to_audit_object_type};
47use crate::command::{CatalogSnapshot, Command};
48use crate::coord::{Coordinator, Message};
49
50const FALLBACK_RECONCILE_INTERVAL: Duration = Duration::from_secs(30);
54
55const RECONCILE_WARN_THRESHOLD: Duration = Duration::from_secs(5);
57
58type InfoGauge = DeleteOnDropGauge<AtomicU64, Vec<String>>;
59
60#[derive(Debug)]
66pub(crate) struct CatalogInfoMetrics {
67 object_info: UIntGaugeVec,
68 cluster_info: UIntGaugeVec,
69 replica_info: UIntGaugeVec,
70 source_info: UIntGaugeVec,
71 sink_info: UIntGaugeVec,
72 series: Vec<InfoGauge>,
76 last_revision: Option<u64>,
79 reconcile_seconds: Histogram,
81}
82
83impl CatalogInfoMetrics {
84 pub fn new(registry: &MetricsRegistry) -> Self {
85 Self {
86 object_info: registry.register(metric!(
87 name: "mz_object_info",
88 help: "Maps catalog object IDs to the object's name, schema, database, and \
89 type. Constant 1.",
90 var_labels: ["object_id", "global_id", "name", "schema_name", "database_name", "type"],
91 visibility: MetricVisibility::Public,
92 tags: [MetricTag::Environment],
93 )),
94 cluster_info: registry.register(metric!(
95 name: "mz_cluster_info",
96 help: "Maps cluster IDs to the cluster's name and size. Constant 1.",
97 var_labels: ["cluster_id", "name", "size"],
98 visibility: MetricVisibility::Public,
99 tags: [MetricTag::Compute],
100 )),
101 replica_info: registry.register(metric!(
102 name: "mz_replica_info",
103 help: "Maps cluster replica IDs to the replica's name and size. Constant 1.",
104 var_labels: ["replica_id", "cluster_id", "name", "size"],
105 visibility: MetricVisibility::Public,
106 tags: [MetricTag::Compute],
107 )),
108 source_info: registry.register(metric!(
109 name: "mz_source_info",
110 help: "Maps user source IDs to the source's type, envelope type, and \
111 cluster. Constant 1.",
112 var_labels: ["source_id", "type", "envelope_type", "cluster_id"],
113 visibility: MetricVisibility::Public,
114 tags: [MetricTag::Source],
115 )),
116 sink_info: registry.register(metric!(
117 name: "mz_sink_info",
118 help: "Maps user sink IDs to the sink's type, envelope type, and \
119 cluster. Constant 1.",
120 var_labels: ["sink_id", "type", "envelope_type", "cluster_id"],
121 visibility: MetricVisibility::Public,
122 tags: [MetricTag::Sink],
123 )),
124 series: Vec::new(),
125 last_revision: None,
126 reconcile_seconds: registry.register(metric!(
127 name: "mz_catalog_info_metrics_reconcile_seconds",
128 help: "Time taken to rebuild the catalog info metrics from a catalog snapshot.",
129 buckets: histogram_seconds_buckets(0.000_128, 8.0),
130 )),
131 }
132 }
133
134 pub fn reconcile(&mut self, catalog: &Catalog) {
137 if self.last_revision != Some(catalog.transient_revision()) {
138 debug!(
139 revision = catalog.transient_revision(),
140 last_revision = self.last_revision,
141 "reconciling catalog info metrics"
142 );
143 let start = Instant::now();
144 self.populate(catalog);
145 let elapsed = start.elapsed();
146 self.reconcile_seconds.observe(elapsed.as_secs_f64());
147 if elapsed > RECONCILE_WARN_THRESHOLD {
148 warn!(
149 ?elapsed,
150 series = self.series.len(),
151 "catalog info metrics reconcile was slow"
152 );
153 }
154 self.last_revision = Some(catalog.transient_revision());
155 }
156 }
157
158 fn populate(&mut self, catalog: &Catalog) {
160 self.series.clear();
164
165 for entry in catalog.entries() {
166 let full_name = catalog.resolve_full_name(entry.name(), entry.conn_id());
167 self.insert_item(entry.id(), entry.item(), &full_name);
168 self.insert_source_or_sink(entry.id(), entry.item());
169 }
170 for cluster in catalog.clusters() {
171 self.insert_cluster(cluster);
172 for replica in cluster.replicas() {
173 self.insert_replica(replica);
174 }
175 }
176 }
177
178 fn insert_item(&mut self, id: CatalogItemId, item: &CatalogItem, full_name: &FullItemName) {
179 match id {
180 CatalogItemId::IntrospectionSourceIndex(_) | CatalogItemId::Transient(_) => return,
183 CatalogItemId::System(_) | CatalogItemId::User(_) => (),
184 }
185
186 let database_name = match &full_name.database {
188 RawDatabaseSpecifier::Name(name) => name.clone(),
189 RawDatabaseSpecifier::Ambient => String::new(),
190 };
191 let item_type = catalog_type_to_audit_object_type(item.typ()).to_string();
192 for global_id in item.global_ids() {
193 let series = new_series(
194 &self.object_info,
195 vec![
196 id.to_string(),
197 global_id.to_string(),
198 full_name.item.clone(),
199 full_name.schema.clone(),
200 database_name.clone(),
201 item_type.clone(),
202 ],
203 );
204 self.series.push(series);
205 }
206 }
207
208 fn insert_source_or_sink(&mut self, id: CatalogItemId, item: &CatalogItem) {
211 let (info_vec, object_type, envelope_type) = match item {
212 CatalogItem::Source(source) => {
213 match &source.data_source {
214 DataSourceDesc::Ingestion { .. }
215 | DataSourceDesc::OldSyntaxIngestion { .. }
216 | DataSourceDesc::Webhook { .. } => (),
217 DataSourceDesc::IngestionExport { .. }
218 | DataSourceDesc::Progress
219 | DataSourceDesc::Introspection(_)
220 | DataSourceDesc::Catalog => return,
221 }
222 (
223 &self.source_info,
224 source.source_type(),
225 source.data_source.envelope(),
226 )
227 }
228 CatalogItem::Sink(sink) => (&self.sink_info, sink.sink_type(), sink.envelope()),
229 _ => return,
230 };
231
232 let series = new_series(
233 info_vec,
234 vec![
235 id.to_string(),
236 object_type.to_string(),
237 envelope_type.map(|s| s.to_string()).unwrap_or_default(),
238 item.cluster_id()
239 .map(|id| id.to_string())
240 .unwrap_or_default(),
241 ],
242 );
243 self.series.push(series);
244 }
245
246 fn insert_cluster(&mut self, cluster: &Cluster) {
247 let size = match &cluster.config.variant {
248 ClusterVariant::Managed(managed) => managed.size.clone(),
249 ClusterVariant::Unmanaged => String::new(),
250 };
251 let series = new_series(
252 &self.cluster_info,
253 vec![cluster.id.to_string(), cluster.name.clone(), size],
254 );
255 self.series.push(series);
256 }
257
258 fn insert_replica(&mut self, replica: &ClusterReplica) {
259 let size = match &replica.config.location {
260 ReplicaLocation::Managed(managed) => managed.size.clone(),
261 ReplicaLocation::Unmanaged(_) => String::new(),
262 };
263 let series = new_series(
264 &self.replica_info,
265 vec![
266 replica.replica_id.to_string(),
267 replica.cluster_id.to_string(),
268 replica.name.clone(),
269 size,
270 ],
271 );
272 self.series.push(series);
273 }
274}
275
276impl Coordinator {
277 pub(crate) fn spawn_catalog_info_metrics_task(&self) {
280 let internal_cmd_tx = self.internal_cmd_tx.clone();
281 let mut metrics = CatalogInfoMetrics::new(&self.catalog_info_metrics_registry);
282 let catalog_arc_strong_count = self.metrics.catalog_arc_strong_count.clone();
283 let catalog_arc_weak_count = self.metrics.catalog_arc_weak_count.clone();
284 task::spawn(|| "catalog_info_metrics", async move {
285 loop {
286 let (tx, rx) = oneshot::channel();
287 let send = internal_cmd_tx.send(Message::Command(
288 OpenTelemetryContext::obtain(),
289 Command::CatalogSnapshot { tx },
290 ));
291 if send.is_err() {
293 break;
294 }
295 let Ok(CatalogSnapshot { catalog }) = rx.await else {
296 break;
297 };
298
299 catalog_arc_strong_count.set(u64::cast_from(Arc::strong_count(&catalog)));
305 catalog_arc_weak_count.set(u64::cast_from(Arc::weak_count(&catalog)));
306
307 let interval =
310 CATALOG_INFO_METRICS_RECONCILE_INTERVAL.get(catalog.system_config().dyncfgs());
311 if !interval.is_zero() {
312 metrics.reconcile(&catalog);
313 }
314
315 let sleep = if interval.is_zero() {
318 FALLBACK_RECONCILE_INTERVAL
319 } else {
320 interval
321 };
322 tokio::time::sleep(sleep).await;
323 }
324 });
325 }
326}
327
328fn new_series(vec: &UIntGaugeVec, labels: Vec<String>) -> InfoGauge {
329 let gauge = vec.get_delete_on_drop_metric(labels);
330 gauge.set(1);
331 gauge
332}
333#[cfg(test)]
334mod tests {
335 use std::collections::{BTreeMap, BTreeSet};
336
337 use mz_catalog::memory::objects::{
338 ClusterConfig, ClusterVariantManaged, Sink, Source, Table, TableDataSource,
339 };
340 use mz_compute_types::config::ComputeReplicaConfig;
341 use mz_controller::clusters::{
342 ManagedReplicaLocation, ReplicaAllocation, ReplicaConfig, UnmanagedReplicaLocation,
343 };
344 use mz_controller_types::{ClusterId, ReplicaId};
345 use mz_repr::adt::mz_acl_item::PrivilegeMap;
346 use mz_repr::role_id::RoleId;
347 use mz_repr::{GlobalId, RelationDesc, RelationVersion, SqlScalarType, VersionedRelationDesc};
348 use mz_sql::names::ResolvedIds;
349 use mz_sql::plan::{WebhookBodyFormat, WebhookHeaders};
350 use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
351 use mz_storage_types::sinks::{
352 KafkaIdStyle, KafkaSinkCompressionType, KafkaSinkConnection, KafkaSinkFormat,
353 KafkaSinkFormatType, SinkEnvelope, StorageSinkConnection,
354 };
355 use mz_storage_types::sources::Timeline;
356
357 use super::*;
358 use crate::catalog::Op;
359
360 fn series(registry: &MetricsRegistry, name: &str) -> Vec<BTreeMap<String, String>> {
362 registry
363 .gather()
364 .iter()
365 .filter(|family| family.name() == name)
366 .flat_map(|family| family.get_metric())
367 .map(|metric| {
368 assert_eq!(metric.get_gauge().value(), 1.0, "info series must be 1");
370 metric
371 .get_label()
372 .iter()
373 .map(|label| (label.name().to_string(), label.value().to_string()))
374 .collect()
375 })
376 .collect()
377 }
378
379 fn labels(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
380 pairs
381 .iter()
382 .map(|(k, v)| (k.to_string(), v.to_string()))
383 .collect()
384 }
385
386 fn test_table(gid: GlobalId) -> CatalogItem {
387 CatalogItem::Table(Table {
388 create_sql: None,
389 desc: VersionedRelationDesc::new(
390 RelationDesc::builder()
391 .with_column("a", SqlScalarType::String.nullable(false))
392 .finish(),
393 ),
394 collections: BTreeMap::from([(RelationVersion::root(), gid)]),
395 conn_id: None,
396 resolved_ids: ResolvedIds::empty(),
397 custom_logical_compaction_window: None,
398 is_retained_metrics_object: false,
399 data_source: TableDataSource::TableWrites { defaults: vec![] },
400 })
401 }
402
403 fn test_webhook_source(gid: u64, cluster_id: ClusterId) -> CatalogItem {
404 CatalogItem::Source(Source {
405 create_sql: None,
406 global_id: GlobalId::User(gid),
407 data_source: DataSourceDesc::Webhook {
408 validate_using: None,
409 body_format: WebhookBodyFormat::Json { array: false },
410 headers: WebhookHeaders::default(),
411 cluster_id,
412 },
413 desc: RelationDesc::builder()
414 .with_column("a", SqlScalarType::String.nullable(false))
415 .finish(),
416 timeline: Timeline::EpochMilliseconds,
417 resolved_ids: ResolvedIds::empty(),
418 custom_logical_compaction_window: None,
419 is_retained_metrics_object: false,
420 })
421 }
422
423 fn test_kafka_sink(gid: u64, cluster_id: ClusterId) -> CatalogItem {
424 CatalogItem::Sink(Sink {
425 create_sql: "CREATE SINK s FROM t INTO KAFKA CONNECTION c (TOPIC 'topic')".to_string(),
426 global_id: GlobalId::User(gid),
427 from: GlobalId::User(1),
428 connection: StorageSinkConnection::Kafka(KafkaSinkConnection {
429 connection_id: CatalogItemId::User(2),
430 connection: CatalogItemId::User(2),
431 format: KafkaSinkFormat {
432 key_format: None,
433 value_format: KafkaSinkFormatType::Json,
434 },
435 relation_key_indices: None,
436 key_desc_and_indices: None,
437 headers_index: None,
438 value_desc: RelationDesc::builder()
439 .with_column("a", SqlScalarType::String.nullable(false))
440 .finish(),
441 partition_by: None,
442 topic: "topic".to_string(),
443 topic_options: Default::default(),
444 compression_type: KafkaSinkCompressionType::None,
445 progress_group_id: KafkaIdStyle::Legacy,
446 transactional_id: KafkaIdStyle::Legacy,
447 topic_metadata_refresh_interval: Duration::from_secs(60),
448 }),
449 envelope: SinkEnvelope::Upsert,
450 with_snapshot: true,
451 version: 0,
452 resolved_ids: ResolvedIds::empty(),
453 cluster_id,
454 commit_interval: None,
455 })
456 }
457
458 fn test_cluster_config(size: &str) -> ClusterConfig {
459 ClusterConfig {
460 variant: ClusterVariant::Managed(ClusterVariantManaged {
461 size: size.to_string(),
462 availability_zones: Vec::new(),
463 logging: Default::default(),
464 replication_factor: 1,
465 optimizer_feature_overrides: Default::default(),
466 schedule: Default::default(),
467 auto_scaling_strategy: None,
468 reconfiguration: None,
469 burst: None,
470 }),
471 workload_class: None,
472 }
473 }
474
475 fn test_cluster(id: ClusterId, name: &str, size: &str) -> Cluster {
476 Cluster {
477 name: name.to_string(),
478 id,
479 config: test_cluster_config(size),
480 log_indexes: BTreeMap::new(),
481 bound_objects: BTreeSet::new(),
482 replica_id_by_name_: BTreeMap::new(),
483 replicas_by_id_: BTreeMap::new(),
484 owner_id: RoleId::User(1),
485 privileges: PrivilegeMap::default(),
486 }
487 }
488
489 fn test_replica(cluster_id: ClusterId, replica_id: ReplicaId, name: &str) -> ClusterReplica {
490 ClusterReplica {
491 name: name.to_string(),
492 cluster_id,
493 replica_id,
494 config: ReplicaConfig {
495 location: ReplicaLocation::Unmanaged(UnmanagedReplicaLocation {
496 storagectl_addrs: Vec::new(),
497 computectl_addrs: Vec::new(),
498 }),
499 compute: ComputeReplicaConfig {
500 logging: Default::default(),
501 },
502 },
503 owner_id: RoleId::User(1),
504 }
505 }
506
507 fn full_name(database: Option<&str>, schema: &str, item: &str) -> FullItemName {
508 FullItemName {
509 database: match database {
510 Some(database) => RawDatabaseSpecifier::Name(database.to_string()),
511 None => RawDatabaseSpecifier::Ambient,
512 },
513 schema: schema.to_string(),
514 item: item.to_string(),
515 }
516 }
517
518 #[mz_ore::test]
519 fn item_insertion_creates_object_info_series() {
520 let registry = MetricsRegistry::new();
521 let mut metrics = CatalogInfoMetrics::new(®istry);
522
523 metrics.insert_item(
524 CatalogItemId::User(1),
525 &test_table(GlobalId::User(1)),
526 &full_name(Some("materialize"), "public", "t"),
527 );
528
529 assert_eq!(
530 series(®istry, "mz_object_info"),
531 vec![labels(&[
532 ("object_id", "u1"),
533 ("global_id", "u1"),
534 ("name", "t"),
535 ("schema_name", "public"),
536 ("database_name", "materialize"),
537 ("type", "table"),
538 ])]
539 );
540 }
541
542 #[mz_ore::test]
543 fn system_items_are_reported() {
544 let registry = MetricsRegistry::new();
545 let mut metrics = CatalogInfoMetrics::new(®istry);
546
547 metrics.insert_item(
548 CatalogItemId::System(456),
549 &test_table(GlobalId::System(456)),
550 &full_name(None, "mz_catalog", "mz_test"),
552 );
553
554 assert_eq!(
555 series(®istry, "mz_object_info"),
556 vec![labels(&[
557 ("object_id", "s456"),
558 ("global_id", "s456"),
559 ("name", "mz_test"),
560 ("schema_name", "mz_catalog"),
561 ("database_name", ""),
562 ("type", "table"),
563 ])]
564 );
565 }
566
567 #[mz_ore::test]
568 fn introspection_indexes_and_temporary_items_are_not_reported() {
569 let registry = MetricsRegistry::new();
570 let mut metrics = CatalogInfoMetrics::new(®istry);
571
572 metrics.insert_item(
573 CatalogItemId::IntrospectionSourceIndex(5),
574 &test_table(GlobalId::IntrospectionSourceIndex(5)),
575 &full_name(None, "mz_introspection", "mz_test"),
576 );
577 metrics.insert_item(
578 CatalogItemId::Transient(6),
579 &test_table(GlobalId::Transient(6)),
580 &full_name(None, "mz_temp", "t"),
581 );
582
583 assert_eq!(series(®istry, "mz_object_info"), Vec::new());
584 }
585
586 #[mz_ore::test]
587 fn user_sources_get_source_info_series() {
588 let registry = MetricsRegistry::new();
589 let mut metrics = CatalogInfoMetrics::new(®istry);
590
591 let cluster_id = ClusterId::user(9).expect("valid id");
592 metrics.insert_source_or_sink(CatalogItemId::User(4), &test_webhook_source(4, cluster_id));
593
594 assert_eq!(
595 series(®istry, "mz_source_info"),
596 vec![labels(&[
597 ("source_id", "u4"),
598 ("type", "webhook"),
599 ("envelope_type", ""),
601 ("cluster_id", "u9"),
602 ])]
603 );
604 }
605
606 #[mz_ore::test]
607 fn user_sinks_get_sink_info_series() {
608 let registry = MetricsRegistry::new();
609 let mut metrics = CatalogInfoMetrics::new(®istry);
610
611 let cluster_id = ClusterId::user(9).expect("valid id");
612 metrics.insert_source_or_sink(CatalogItemId::User(5), &test_kafka_sink(5, cluster_id));
613
614 assert_eq!(
615 series(®istry, "mz_sink_info"),
616 vec![labels(&[
617 ("sink_id", "u5"),
618 ("type", "kafka"),
619 ("envelope_type", "upsert"),
620 ("cluster_id", "u9"),
621 ])]
622 );
623 }
624
625 #[mz_ore::test]
626 fn cluster_and_replica_insertions_create_info_series() {
627 let registry = MetricsRegistry::new();
628 let mut metrics = CatalogInfoMetrics::new(®istry);
629
630 let cluster_id = ClusterId::user(7).expect("valid id");
631 let replica_id = ReplicaId::User(2);
632 metrics.insert_cluster(&test_cluster(cluster_id, "prod", "123cc"));
633 metrics.insert_replica(&test_replica(cluster_id, replica_id, "r1"));
634
635 assert_eq!(
636 series(®istry, "mz_cluster_info"),
637 vec![labels(&[
638 ("cluster_id", "u7"),
639 ("name", "prod"),
640 ("size", "123cc"),
641 ])]
642 );
643 assert_eq!(
644 series(®istry, "mz_replica_info"),
645 vec![labels(&[
646 ("replica_id", "u2"),
647 ("cluster_id", "u7"),
648 ("name", "r1"),
649 ("size", ""),
652 ])]
653 );
654 }
655
656 #[mz_ore::test]
657 #[cfg_attr(miri, ignore)] fn replicas_of_unmanaged_clusters_report_their_size() {
659 let registry = MetricsRegistry::new();
660 let mut metrics = CatalogInfoMetrics::new(®istry);
661
662 let allocation: ReplicaAllocation =
666 serde_json::from_str(r#"{"scale": 2, "workers": 4, "credits_per_hour": "0"}"#)
667 .expect("valid allocation");
668 let replica = ClusterReplica {
669 name: "r2".to_string(),
670 cluster_id: ClusterId::user(7).expect("valid id"),
671 replica_id: ReplicaId::User(3),
672 config: ReplicaConfig {
673 location: ReplicaLocation::Managed(ManagedReplicaLocation {
674 allocation,
675 size: "scale=2,workers=4".to_string(),
676 internal: false,
677 billed_as: None,
678 availability_zones: Vec::new(),
679 pending: false,
680 }),
681 compute: ComputeReplicaConfig {
682 logging: Default::default(),
683 },
684 },
685 owner_id: RoleId::User(1),
686 };
687 metrics.insert_replica(&replica);
688
689 assert_eq!(
690 series(®istry, "mz_replica_info"),
691 vec![labels(&[
692 ("replica_id", "u3"),
693 ("cluster_id", "u7"),
694 ("name", "r2"),
695 ("size", "scale=2,workers=4"),
696 ])]
697 );
698 }
699
700 #[mz_ore::test(tokio::test)]
701 #[cfg_attr(miri, ignore)]
702 async fn reconcile_rebuilds_from_the_catalog_when_its_revision_changes() {
703 Catalog::with_debug(|mut catalog| async move {
704 let registry = MetricsRegistry::new();
705 let mut metrics = CatalogInfoMetrics::new(®istry);
706
707 let has_test_cluster_series = |registry: &MetricsRegistry| {
708 series(registry, "mz_cluster_info")
709 .iter()
710 .any(|s| s["name"] == "test_cluster")
711 };
712
713 metrics.reconcile(&catalog);
714
715 assert!(
718 series(®istry, "mz_cluster_info")
719 .iter()
720 .any(|s| s["name"] == "mz_system")
721 );
722 assert_ne!(series(®istry, "mz_replica_info"), Vec::new());
723 let objects = series(®istry, "mz_object_info");
724 assert!(objects.iter().any(|s| s["object_id"].starts_with("s")));
726 assert!(!objects.iter().any(|s| s["object_id"].starts_with("si")));
728
729 assert!(!has_test_cluster_series(®istry));
731
732 let commit_ts = catalog.current_upper().await;
734 let cluster_id = catalog
735 .allocate_user_cluster_id(commit_ts)
736 .await
737 .expect("failed to allocate cluster id");
738 let commit_ts = catalog.current_upper().await;
739 catalog
740 .transact(
741 None,
742 commit_ts,
743 None,
744 vec![Op::CreateCluster {
745 id: cluster_id,
746 name: "test_cluster".to_string(),
747 introspection_sources: Vec::new(),
748 owner_id: MZ_SYSTEM_ROLE_ID,
749 config: test_cluster_config("scale=1,workers=2"),
750 }],
751 )
752 .await
753 .expect("failed to transact");
754
755 assert!(!has_test_cluster_series(®istry));
757
758 metrics.reconcile(&catalog);
759 assert!(has_test_cluster_series(®istry));
760
761 catalog.expire().await;
762 })
763 .await
764 }
765}